Compare commits

...

73 Commits

Author SHA1 Message Date
cb9bfcff12 tester(ET): auto-commit from tester run_id=457
All checks were successful
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 31s
2026-06-09 14:14:30 +03:00
d846910ca6 reviewer(ET): auto-commit from reviewer run_id=456 2026-06-09 14:14:30 +03:00
92961d1d32 refactor(frontmatter): unified frontmatter contract + handoff spec (ORCH-52c)
src/frontmatter.py grows from a single-key reader into the full machine
contract: reader (read_frontmatter_value, unchanged), one parse primitive
(parse_frontmatter), writer (render/write_frontmatter), schema validator
(validate_schema/REQUIRED_FIELDS, warning-only by default) and a shared
strip_frontmatter helper. The five verdict gates (check_reviewer_verdict,
_parse_tests_verdict, _parse_deploy_status, _parse_staging_status,
parse_security_status) now read through the single parse_frontmatter point
instead of duplicated ad-hoc YAML logic; review_parse._strip_frontmatter and
security_gate.extract_security_findings reuse the shared helper.

Strictly backward compatible + never-raise: STAGE_TRANSITIONS, the QG_CHECKS
composition, verdict semantics (incl. ORCH-047 three-field tester + negative
token priority), reason-strings and worktree->origin/main fallback are 1:1.
The schema validator never influences a gate verdict by default; hard-fail is
reserved behind the frontmatter_validation_strict kill-switch (default False).

New formal handoff spec docs/_standards/HANDOFF_PROTOCOL.md ("stage -> required
output" + required frontmatter schema), aligned 1:1 with PIPELINE_DOCS.md.

Tests: test_frontmatter.py (TC-01..07), test_qg_verdicts.py (TC-08..15),
test_security_gate.py (TC-12), test_stages_invariants.py (TC-16). Full
tests/ green (1212).

Refs: ORCH-076

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:14:30 +03:00
2030d1627a architect(ET): auto-commit from architect run_id=454 2026-06-09 14:14:30 +03:00
98c50a094b analyst(ET): auto-commit from analyst run_id=453 2026-06-09 14:14:30 +03:00
561f58abe0 docs: init ORCH-076 business request 2026-06-09 14:14:30 +03:00
95d2c2093a Merge pull request 'docs(ORCH-076): staging gate log — SUCCESS' (#93) from docs/ORCH-076-staging-log into main 2026-06-09 14:13:59 +03:00
6868e34d1f docs(ORCH-076): staging gate log — staging_status SUCCESS
All checks were successful
CI / test (pull_request) Successful in 32s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:13:42 +03:00
03c6f2a145 Merge pull request 'ORCH-075 — ORCH-52b: стандарт документов (docs/_standards + docs/_templates + ADR-naming)' (#91) from feature/ORCH-075-orch-52b-docs-templates-adr-na into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 13:31:18 +03:00
deploy-finalizer
da3e6e8acd deploy(ORCH-036): finalize SUCCESS for ORCH-075
All checks were successful
CI / test (push) Successful in 31s
2026-06-09 13:31:17 +03:00
119b8f2bec tester(ET): auto-commit from tester run_id=451
All checks were successful
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 32s
2026-06-09 13:25:39 +03:00
138092e040 reviewer(ET): auto-commit from reviewer run_id=450 2026-06-09 13:25:39 +03:00
5e60543232 docs(standards): pipeline docs standard — manifest + templates + ADR-naming
Создан golden source структуры номерных документов work item (ORCH-52b, слой 1
эпика ORCH-52). Docs-only: STAGE_TRANSITIONS / QG_CHECKS / check_* / схема БД не
трогаются (AC-6).

- docs/_standards/PIPELINE_DOCS.md — манифест «стадия→агент→документ→категория→
  гейт→frontmatter machine-key» (сверен с src/stages.py и src/qg/checks.py) +
  раздел ADR-naming. Манифест документирует поведение гейтов, источник истины
  остаётся код (ADR-001 §D2); честно различает machine-verdict (12/13/14/15/17)
  и информационные (00/08/10/16) доки; под-гейты ребра deploy-staging→deploy
  отмечены как врезки в advance_stage.
- docs/_templates/* — 15 копируемых скелетов; машинные доки несут точный
  frontmatter-ключ из _parse_* (verdict/result/deploy_status/staging_status/
  security_status/post_deploy_status).
- Точки-ссылки: CLAUDE.md, docs/architecture/README.md; запись CHANGELOG.
- tests/test_orch_52b_docs_standard.py — TC-01..TC-20 структурные проверки;
  полный pytest tests/ зелёный (1177 passed).

Refs: ORCH-075

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:25:39 +03:00
3251c8c4ed architect(ET): auto-commit from architect run_id=448 2026-06-09 13:25:39 +03:00
6511ddadbb analyst(ET): auto-commit from analyst run_id=447 2026-06-09 13:25:39 +03:00
18e98945dd docs: init ORCH-075 business request 2026-06-09 13:25:39 +03:00
0f7db904f1 deploy-staging(ORCH-075): staging gate SUCCESS
Staging suite 8/10 PASS, exit 0. All REAL checks green; C9a/C9b
sandbox-infra waived (ORCH-061). staging_status: SUCCESS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:25:04 +03:00
73d936a4c4 Merge pull request 'feat(labels): auto-mode by Plane labels — autoApprove + autoDeploy (ORCH-089)' (#89) from feature/ORCH-089-autoapprove-brd-autodeploy into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 12:38:52 +03:00
deploy-finalizer
a16196d68c deploy(ORCH-036): finalize SUCCESS for ORCH-089
All checks were successful
CI / test (push) Successful in 30s
2026-06-09 12:38:51 +03:00
9b3490ceaa tester(ET): auto-commit from tester run_id=445
All checks were successful
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 29s
2026-06-09 12:31:24 +03:00
3c407397da reviewer(ET): auto-commit from reviewer run_id=444 2026-06-09 12:31:24 +03:00
a6d0ba51c0 feat(labels): auto-mode by Plane labels — autoApprove + autoDeploy (ORCH-089)
Lift the two HUMAN gates that block an autonomous batch run (epic ORCH-088):
the BRD gate (analysis: manual Approved) and the prod-deploy gate (deploy
Phase A: manual Confirm Deploy, ORCH-059). Selective (a Plane label on the
issue), declarative, reversible, and WITHOUT touching a single technical check.

Additive, mirroring the conditional sub-gates (ORCH-035/043/058/088): leaf
src/labels.py (never-raise) + two point insertions + config flags.
STAGE_TRANSITIONS / QG_CHECKS / check_* / DB schema are NOT touched.

- autoApprove: врезка in _handle_analysis_approved_flow (files_ok branch) ->
  set_issue_approved + log/Telegram/Plane-comment + advance_stage(
  finished_agent=None) — the SAME path a human Approved takes (approved-via-
  status -> analysis->architecture + mark_brd_review_ended). No duplicated
  transition logic; re-entrancy safe.
- autoDeploy: врезка in _handle_self_deploy_phase_a after advance to deploy +
  clear_state -> log/Telegram/Plane-comment + _handle_self_deploy_phase_b
  (INITIATED marker, Deploying, finalizer). Only the indicative human steps are
  skipped. BR-5 holds structurally: Phase A is reached only after the green edge
  sub-gates, so autoDeploy can never deploy a broken build.
- plane_sync: fetch_issue_labels (None on error != []), get_project_labels
  ({normalized_name->uuid}, TTL cache, ambiguity sentinel), set_issue_approved.
- config flags: auto_label_enabled (kill-switch), auto_approve_label/
  auto_deploy_label, auto_label_repos (empty -> self-hosting only),
  auto_label_states_ttl_s. applies() (local) checked FIRST; has_label (network)
  only when applies==True -> zero network / zero regression when disabled (AC-8).
- Fail-safe (never auto on doubt), transparency via log+Telegram+Plane+card,
  read-only auto_labels block in GET /queue.
- Tests TC-01..TC-26 across 7 modules; docs (CLAUDE.md, architecture README,
  CHANGELOG) updated in the same PR.

Refs: ORCH-089

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:31:24 +03:00
f7488e9536 architect(ET): auto-commit from architect run_id=442 2026-06-09 12:31:24 +03:00
0b5fede802 analyst(ET): auto-commit from analyst run_id=441 2026-06-09 12:31:24 +03:00
cc2f1885e8 docs: init ORCH-089 business request 2026-06-09 12:31:24 +03:00
c9be0eb4c9 Merge pull request 'docs(ORCH-089): staging gate SUCCESS — 15-staging-log.md' (#90) from chore/ORCH-089-staging-log into main 2026-06-09 12:30:52 +03:00
21bde85708 docs(ORCH-089): staging gate SUCCESS — 15-staging-log.md
All checks were successful
CI / test (pull_request) Successful in 29s
Staging check 8/10 PASS, exit 0. All REAL checks green; C9a/C9b
sandbox-infra waived (ORCH-061).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:30:34 +03:00
7d61c820a7 Merge pull request 'feat(serial-gate): per-repo serial gate + deferred branch cut + rollback-freeze (ORCH-088)' (#88) from feature/ORCH-088-orch-88-10-20 into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 11:31:58 +03:00
deploy-finalizer
69f493fec5 deploy(ORCH-036): finalize SUCCESS for ORCH-088
All checks were successful
CI / test (push) Successful in 29s
2026-06-09 11:31:57 +03:00
dd4aaebe84 tester(ET): auto-commit from tester run_id=439
All checks were successful
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 32s
2026-06-09 11:24:48 +03:00
f645090e4d reviewer(ET): auto-commit from reviewer run_id=438 2026-06-09 11:24:48 +03:00
ee4773f5b0 feat(serial-gate): per-repo serial gate + deferred branch cut + rollback-freeze (ORCH-088)
Этап 1 (serial e2e) пакетного автономного режима. Новая задача репо не входит
в analysis (analyst-job не выбирается, ветка не режется), пока в репо есть более
ранняя незавершённая задача (FIFO, t2.id < jobs.task_id) ИЛИ репо заморожен.

- src/serial_gate.py — новый leaf (never-raise): build_claim_clause (fail-OPEN),
  is_repo_frozen (fail-CLOSED), set/clear_repo_freeze, serial_gate_applies, snapshot.
- src/db.py — идемпотентная миграция repo_freeze + serial_gate-фрагмент в claim_next_job.
- src/webhooks/plane.py + src/agents/launcher.py — отложенный срез ветки: start_pipeline
  не создаёт Gitea-ветку/docs для применимого репо; релокация в _materialize_deferred_branch
  на момент claim analyst-job (база = свежий origin/main с кодом предшественника, AC-6).
- src/stage_engine.py — post-deploy DEGRADED → durable per-repo freeze + Telegram-алерт.
- src/main.py — блок serial_gate в GET /queue + POST /serial-gate/unfreeze.
- src/config.py — serial_gate_enabled / serial_gate_repos / serial_gate_freeze_enabled.

FIFO-уточнение реализации (FR-2): ADR-001 D1 фиксировал t2.id != jobs.task_id; при !=
пакет одновременно созданных свежих задач взаимно блокировался бы (дедлок). t2.id <
jobs.task_id допускает самую раннюю задачу и сериализует остальные, сохраняя AC-1/R-7.

STAGE_TRANSITIONS / QG_CHECKS / check_* — без изменений. Аддитивно, под kill-switch,
never-raise, restart-safe; при выключенном флаге — нулевая регрессия (enduro не затронут).

Тесты: TC-01..TC-22 (test_serial_gate*.py + test_queue_endpoint.py); полный прогон 1114 зелёных.
Docs: README (serial gate / /queue / API / БД), CLAUDE.md, CHANGELOG.md, .env.example.

Refs: ORCH-088
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:24:48 +03:00
4597a8471d architect(ET): auto-commit from architect run_id=436 2026-06-09 11:24:48 +03:00
b478b38df5 analyst(ET): auto-commit from analyst run_id=435 2026-06-09 11:24:48 +03:00
99cafefba6 docs: init ORCH-088 business request 2026-06-09 11:24:48 +03:00
85cfce451f docs(ORCH-088): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:24:23 +03:00
a23d4c0971 Merge pull request 'fix(notifications): tracker orphan cleanup + effort + honest done-time (ORCH-087)' (#87) from feature/ORCH-087-orch-87-to-analyse-bump into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 10:12:06 +03:00
deploy-finalizer
49fad5e458 deploy(ORCH-036): finalize SUCCESS for ORCH-087
All checks were successful
CI / test (push) Successful in 28s
CI / test (pull_request) Successful in 28s
2026-06-09 10:12:05 +03:00
d9bb8d5fe3 tester(ET): auto-commit from tester run_id=433
All checks were successful
CI / test (push) Successful in 32s
CI / test (pull_request) Successful in 30s
2026-06-09 10:06:17 +03:00
32cc965f84 reviewer(ET): auto-commit from reviewer run_id=432 2026-06-09 10:06:17 +03:00
81fc2df8a8 fix(launcher): runs log dir from settings, not hardcoded /app (CI fix)
test_spawn_stamps_resolved_effort упал в CI с PermissionError на '/app':
launcher._spawn хардкодил output_path='/app/data/runs/{run_id}.log' и
os.makedirs('/app/data/runs'). В контейнере /app есть, на CI-хосте
(act_runner hostexecutor) — нет, makedirs бросает -> красный CI.

Фикс корня (не только теста): базовый каталог per-run логов вынесен в
Settings.runs_dir (env ORCH_RUNS_DIR, дефолт '/app/data/runs' = прод 1:1).
Новый хелпер _run_log_path(run_id) — единый источник пути, использован в
_spawn + три прежних inline-строки логов/алертов. Тест monkeypatch-ит
settings.runs_dir на tmp_path -> окружение-независим (проверено прогоном
с принудительно недоступным /app). pytest tests/ -q: 1090 passed.

STAGE_TRANSITIONS/QG_CHECKS/схема БД не тронуты. Docs: README env-таблица,
CHANGELOG.

Refs: ORCH-087
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:06:17 +03:00
a7b27f2235 fix(notifications): tracker orphan cleanup + effort-in-line + honest done-time (ORCH-087)
Устраняет «замёрзшие» осиротевшие карточки live-трекера и доделывает строку
стадии/итоговое время.

G1 — зачистка сирот: аддитивный леджер tracker_messages(task_id, message_id,
created_at, deleted_at) + хелперы add/get_open/mark_deleted в src/db.py. bump
теперь удаляет ВСЕ незакрытые mid задачи (а не только скаляр
tasks.tracker_message_id, сохранён как BC-указатель). Новый mid в леджер только
при успешном send (BR-6); transient-delete остаётся для ретрая; «already
gone»/>48ч закрывается. Корень бага — скалярный учёт, терявший ссылку при
гонке/delete-fail+send-ok (ADR-001 G0).

G3 — deploy-цикл: ключ confirm_deploy в _LIVE_BRANCH_LABELS (без base-alias).

BR-EFF — эффорт в строке: колонка agent_runs.effort (_ensure_column,
идемпотентно), стамп фактического resolve_agent_effort в launcher._spawn в
момент запуска; рендер `· {model} · {effort}`, пустой → суффикс опускается.

BR-G5 — честное время: done-строка `⏱️ Агенты Σ · твоё {review~cap} · общее с
ожиданием {wall}` — три независимых подписанных метрики; кап
tracker_brd_review_cap_s (ORCH_TRACKER_BRD_REVIEW_CAP_S, дефолт 2ч, маркер ~).

Инварианты: STAGE_TRANSITIONS/QG_CHECKS/стадии без изменений; миграции
аддитивны/идемпотентны (enduro не трогается); never-raise,
disable_notification, plane_issue_link (ORCH-067), disable_web_page_preview
(ORCH-080) сохранены; src/reconciler.py не эродирован (ORCH-086 на месте).

Тесты: tests/test_notifications_orphans.py (TC-01..05 + never-raise),
tests/test_tracker_effort_time.py (TC-06/11..15 + confirm_deploy),
tests/test_launcher.py::TestEffortStamp (TC-09/10). Доки: CLAUDE.md
(§Нотификации), docs/architecture/README.md (Notifications), CHANGELOG.md.

Refs: ORCH-087

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:06:17 +03:00
36c7a68722 architect(ET): auto-commit from architect run_id=429 2026-06-09 10:06:17 +03:00
18fb2eb17d analyst(ET): auto-commit from analyst run_id=428 2026-06-09 10:06:17 +03:00
c86dc3ca95 docs: init ORCH-087 business request 2026-06-09 10:06:17 +03:00
77714aa318 docs(ORCH-087): staging gate SUCCESS — 15-staging-log.md
Staging check suite passed (8/10 PASS, exit 0). C9a/C9b waived as
sandbox-infra (ORCH-061); all REAL checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:05:45 +03:00
493b9be9c4 Merge pull request 'fix(reconciler): terminal-skip + state_uuid dedup on F-1 path (ORCH-086)' (#86) from feature/ORCH-086-orch-86-reconciler-telegram-et into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 05:00:12 +03:00
deploy-finalizer
1b095282bf deploy(ORCH-036): finalize SUCCESS for ORCH-086
All checks were successful
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 26s
2026-06-09 05:00:11 +03:00
9c19588bcd tester(ET): auto-commit from tester run_id=424
All checks were successful
CI / test (push) Successful in 28s
CI / test (pull_request) Successful in 30s
2026-06-09 02:26:49 +03:00
fe3f1658ba reviewer(ET): auto-commit from reviewer run_id=423 2026-06-09 02:26:49 +03:00
595c382ac7 fix(reconciler): terminal-skip + state_uuid dedup on F-1 path
Закрывает F-1-пробел ORCH-068: терминал-исключение и in-memory dedup
(изначально только F-2) распространены на gate-side путь реконсилятора,
устраняя ложное «🔧 reconciler: ET-002 done разблокирована (потерян
webhook)» (особенно после рестарта).

- D1: новый _resolve_issue_status — один сетевой резолв Plane-статуса
  задачи за тик (states, groups, state_uuid) после дешёвых локальных
  гардов; never-raise -> ({}, {}, None) при сбое.
- D2: безусловный терминал-скип ДО Guard 2 (группа Plane completed/
  cancelled, fallback на логические ключи done/cancelled, либо стадия в
  БД орка ∈ {done, cancelled}); skipped_terminal_total++, не подчинён
  reconcile_skip_blocked_enabled.
- D3: _is_blocked_or_needs_input переиспользует резолв D1 (опц. аргументы,
  _UNSET -> самостоятельный резолв для прямых/легаси-вызовов; 1:1).
- D4: вызов _note_unblock на F-1 теперь передаёт state_uuid -> dedup
  работает на обоих путях (deduped_total++ на повторе).

Анти-регресс: легитимный unblock не-терминальной застрявшей задачи
по-прежнему advance + один Telegram. STAGE_TRANSITIONS / QG_CHECKS /
схема БД / сигнатуры advance_*/_note_unblock / форма status() / новые
флаги — без изменений; never-raise сохранён.

Тесты: tests/test_reconciler.py TC-86-01..09/11,
tests/test_reconciler_plane.py TC-86-10. Полный прогон зелёный (1069).

Refs: ORCH-086
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:26:49 +03:00
aa488edddf architect(ET): auto-commit from architect run_id=421 2026-06-09 02:26:49 +03:00
f2161451a0 analyst(ET): auto-commit from analyst run_id=419 2026-06-09 02:26:49 +03:00
0e7d608fc0 docs: init ORCH-086 business request 2026-06-09 02:26:49 +03:00
fb9390e216 docs(ORCH-086): staging gate log — SUCCESS (infra-waived C9a/C9b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:26:25 +03:00
92817889c4 Merge pull request 'fix: disable Telegram link-preview in tracker notifications (ORCH-080)' (#85) from feature/ORCH-080-orch-52g-telegram-link-preview into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 01:38:16 +03:00
deploy-finalizer
baf7860822 deploy(ORCH-036): finalize SUCCESS for ORCH-080
All checks were successful
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 24s
2026-06-09 01:38:15 +03:00
2cf40c1af9 tester(ET): auto-commit from tester run_id=417
All checks were successful
CI / test (push) Successful in 28s
CI / test (pull_request) Successful in 25s
2026-06-09 01:32:53 +03:00
44ef0bb570 reviewer(ET): auto-commit from reviewer run_id=416 2026-06-09 01:32:53 +03:00
d826eacfcf fix: disable Telegram link-preview in tracker notifications (ORCH-080)
Add "disable_web_page_preview": True to the JSON payload of both
low-level Telegram primitives — send_telegram (POST /sendMessage) and
edit_telegram (POST /editMessageText). Telegram no longer expands the
Plane "Modern project management" link-preview banner under every
tracker card (bump/edit) and notify/alert message, which the default
bump mode (ORCH-067) was duplicating on each transition.

Single-point fix at the primitive level — all consumers
(update_task_tracker, notify_approve_requested, notify_error, stage
alerts from launcher/stage_engine) inherit it without code changes.
parse_mode: HTML is preserved so the ORCH-NNN issue link stays
clickable; disable_notification, bump/edit logic, the one-card-per-task
invariant, return contracts and never-raise are untouched. Unconditional,
no kill-switch (ADR-001).

Tests: tests/test_link_preview_disabled.py (TC-01..06). Docs: CHANGELOG,
CLAUDE.md, docs/architecture/README.md (Notifications component).

Refs: ORCH-080
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 01:32:53 +03:00
a482b36dae architect(ET): auto-commit from architect run_id=414 2026-06-09 01:32:53 +03:00
f452626bb8 analyst(ET): auto-commit from analyst run_id=413 2026-06-09 01:32:53 +03:00
b46fc6e51b docs: init ORCH-080 business request 2026-06-09 01:32:53 +03:00
140827f4da docs(ORCH-080): merge staging gate log (staging_status: SUCCESS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 01:32:32 +03:00
fc29ba76ec Merge pull request 'feat(merge-verify): guarantee idempotent open code-PR before merge_pr (ORCH-082)' (#82) from feature/ORCH-082-orch-81-pr-merge-verify-hold into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-09 01:06:25 +03:00
deploy-finalizer
9834dae108 deploy(ORCH-036): finalize SUCCESS for ORCH-082
All checks were successful
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 24s
2026-06-09 01:01:56 +03:00
039322001a tester(ET): auto-commit from tester run_id=411
All checks were successful
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 25s
2026-06-09 00:57:08 +03:00
1997376eb5 reviewer(ET): auto-commit from reviewer run_id=410 2026-06-09 00:57:08 +03:00
0ab6a33ef5 feat(merge-verify): guarantee idempotent open code-PR before merge_pr (ORCH-082)
Close the missing invariant "by merge-verify time the branch has an open
code-PR". The pipeline created a PR only on the developer path with a fresh
worktree commit (launcher._ensure_pr), so a branch (e.g. after a manual main
restore) could reach the deploy->done merge-verify under-gate PR-less ->
merge_pr returned "no open PR" -> a FALSE HOLD (ORCH-074 incident).

- merge_gate.ensure_open_pr(repo, branch) -> (status, detail): idempotent
  leaf-actor (never-raise). GET open PRs filtered head==branch AND base==main
  (identical to merge_pr/ORCH-073 FR-3 — auto docs-PR is not a code-PR) ->
  existed; else POST -> created; 409/422 race -> re-GET -> existed (no dup);
  any other error -> failed.
- stage_engine._handle_merge_verify: врезка after validated_revision and
  BEFORE merge_pr. created|existed -> proceed; failed -> honest HOLD via new
  _hold_pr_create_failed (note "pr-create-failed-hold", text distinguishable
  from the not-merged HOLD; task stays on deploy, NO rollback).
- launcher._ensure_pr delegated to ensure_open_pr (single PR-creation path,
  shared head==branch & base==main filter); the developer-only trigger is
  unchanged.
- ORCH-073 protection untouched & authoritative: merge is confirmed ONLY by
  verify_merged_to_main (SHA-in-main) + check_main_regression. Real un-merged
  code still HOLDs.
- Kill-switch ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED (default true); scope =
  merge_verify_applies (self-hosting / merge_verify_repos); non-self -> no-op;
  false -> ORCH-074 behaviour 1:1. No DB migration; main never push/force-push.
- Append ORCH-082 marker to MAIN_REGRESSION_MARKERS (append-only convention).
- conftest defaults the autocreate flag OFF (mirrors merge_verify_enabled) so
  unrelated deploy->done tests stay 1:1 (no network).

Tests: tests/test_orch082_ensure_pr.py (TC-01..05),
tests/test_orch082_merge_verify_autocreate.py (TC-06..12). Docs: README
merge-verify block (ORCH-082), CHANGELOG, .env.example.

Refs: ORCH-082

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 00:57:08 +03:00
74269b467c architect(ET): auto-commit from architect run_id=408 2026-06-09 00:57:08 +03:00
781f9df26c analyst(ET): auto-commit from analyst run_id=407 2026-06-09 00:57:08 +03:00
c0715ad55b docs: init ORCH-082 business request 2026-06-09 00:57:08 +03:00
7ee528ad7b Merge pull request 'docs(ORCH-082): staging gate log — SUCCESS' (#83) from docs/ORCH-082-staging-log into main 2026-06-09 00:56:37 +03:00
168 changed files with 14229 additions and 187 deletions

View File

@@ -107,6 +107,20 @@ ORCH_PREMERGE_REBASE_ALWAYS=true
# cache them into job_deps (the scheduler then reads only the DB).
ORCH_TASK_DEPS_ENABLED=true
ORCH_TASK_DEPS_SOURCE=db
# ORCH-088 (Stage 1, serial e2e): per-repo serial gate. A NEW task's analyst-job does
# NOT enter analysis (no branch cut, no analyst) while the same repo has an EARLIER
# unfinished task (FIFO, tasks.id < the job's task) OR the repo is frozen. The branch
# cut is DEFERRED from start_pipeline to the analyst-job claim so its base is a fresh
# origin/main already containing the predecessor (anti-stale-base). Gate lives in
# claim_next_job (offline hot-path, fail-OPEN on error); freeze (FR-5) is a durable
# repo_freeze row set on post-deploy DEGRADED, cleared manually via
# POST /serial-gate/unfreeze?repo=<repo>. Leaf src/serial_gate.py (never-raise).
# SERIAL_GATE_ENABLED=false -> claim AND start_pipeline are 1:1 as before ORCH-088.
# SERIAL_GATE_REPOS (CSV) -> scope; EMPTY = ALL repos (not self-hosting-only).
# SERIAL_GATE_FREEZE_ENABLED=false -> the rollback-freeze layer is off (not set/read).
ORCH_SERIAL_GATE_ENABLED=true
ORCH_SERIAL_GATE_REPOS=
ORCH_SERIAL_GATE_FREEZE_ENABLED=true
# ORCH-071/073: merge-verify under-gate on the `deploy -> done` edge (врезка in
# advance_stage, NOT a new STAGE_TRANSITIONS edge / registered QG). A deterministic
# merge-actor merges the feature code-PR via the Gitea PR-merge API (never push/
@@ -123,11 +137,17 @@ ORCH_TASK_DEPS_SOURCE=db
# REGRESSION_GUARD_ENABLED -> kill-switch for the ORCH-073 main-integrity regression
# guard (false -> SHA-in-main alone gates done); reuses the
# merge-verify scope, so non-self repos are a no-op.
# MERGE_VERIFY_AUTOCREATE_PR_ENABLED -> ORCH-082: guarantee an open code-PR
# (head==branch, base==main) via merge_gate.ensure_open_pr
# BEFORE the deterministic merge_pr (fixes the false HOLD
# "no open PR"). false -> exactly pre-ORCH-082 behaviour.
# Reuses the merge-verify scope; non-self repos -> no-op.
ORCH_MERGE_VERIFY_ENABLED=true
ORCH_MERGE_VERIFY_REPOS=
ORCH_MERGE_PR_TIMEOUT_S=60
ORCH_MERGE_VERIFY_TIMEOUT_S=60
ORCH_REGRESSION_GUARD_ENABLED=true
ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=true
# ORCH-036: executable self-deploy of the `deploy` stage. For the self-hosting repo
# (orchestrator) the stage REALLY restarts prod (8500) via a detached host hook;
# deploy_status: SUCCESS means proven health-ok, not an LLM declaration. Three

View File

@@ -1,4 +1,4 @@
Work item: ORCH-061
Work item: ORCH-088
Repo: orchestrator
Branch: feature/ORCH-061-bug-deploy-staging-development
Branch: feature/ORCH-088-orch-88-10-20
Stage: development

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
- Backend: FastAPI + uvicorn (Python 3.12)
- БД: SQLite (`src/db.py`)
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break).
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`).
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). **ORCH-088 (serial gate, Этап 1):** новая задача репо не входит в `analysis` (analyst-job не выбирается, ветка не режется), пока в репо есть **более ранняя** незавершённая задача (`t2.id < jobs.task_id`, FIFO) ИЛИ репо заморожен (`repo_freeze`). Срез ветки **отложен** со `start_pipeline` на момент claim analyst-job (`launcher._materialize_deferred_branch`) — база = свежий `origin/main` с кодом предшественника (анти-stale-base). Post-deploy `DEGRADED` → durable per-repo freeze (`repo_freeze`, `cleared_at IS NULL` = активен) + Telegram; снятие — вручную `POST /serial-gate/unfreeze?repo=…`. Leaf `src/serial_gate.py` (claim — fail-OPEN, freeze — fail-CLOSED); флаги `ORCH_SERIAL_GATE_ENABLED` (kill-switch), `ORCH_SERIAL_GATE_REPOS` (CSV; пусто = все репо), `ORCH_SERIAL_GATE_FREEZE_ENABLED`. Блок `serial_gate` в `GET /queue`. `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты.
- Контейнеризация: Docker + Compose
- CI/CD: Gitea Actions (`.gitea/workflows/`)
- Деплой: docker compose на mva154
@@ -41,36 +41,92 @@ created → analysis → architecture → development → review → testing →
## Статусная модель 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 live-tracker (ORCH-042/066/067/087)
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
- **Дефолт `tracker_mode``bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
- **Зачистка сирот (ORCH-087):** bump ведёт авторитетный леджер ВСЕХ созданных карточек
(таблица `tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении удаляет
ВСЕ незакрытые mid, а не только скаляр `tracker_message_id` (он сохранён как указатель на
текущую карточку, BC). Это устраняет класс «замёрзшая сирота» (старая карточка с заголовком
ранней стадии, потерявшая ссылку при гонке/`delete`-fail+`send`-ok). Новый mid пишется в
леджер ТОЛЬКО при успешном `send` (BR-6); transient-`delete` остаётся незакрытым для ретрая;
«already gone»/>48ч (`_DELETE_GONE_MARKERS`) → закрывается. Остаточная гонка самозалечивается
за один bump. Known-limitation: Telegram 48ч (сироты старше неудаляемы).
- **Эффорт в строке стадии (ORCH-087):** колонка `agent_runs.effort` стампится фактическим
`resolve_agent_effort` в `launcher._spawn` (CLI его в result-JSON не возвращает); строка
рендерится `· {model} · {effort}` (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
пустой/исторический effort → суффикс опускается.
- **Честное итоговое время (ORCH-087):** done-строка = три независимых подписанных метрики
`⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall}` (раньше `Всего {wall}`
читалось как сумма, которой не является). «Твоё» ограничено `tracker_brd_review_cap_s`
(`ORCH_TRACKER_BRD_REVIEW_CAP_S`, дефолт 2ч; маркер `~` при отсечке аномального застоя).
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
Blocked / Rejected / Cancelled / Deploying / Monitoring) и **никогда не блокирует конвейер**.
Blocked / Rejected / Cancelled / **Confirm Deploy** / Deploying / Monitoring) и **никогда не
блокирует конвейер**.
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
- **Без link-preview (ORCH-080):** оба примитива (`send_telegram`/`edit_telegram`) шлют
payload с `disable_web_page_preview: True` — баннер Plane («Modern project management»)
под кликабельной ссылкой `ORCH-NNN` больше не разворачивается ни в карточке (`bump`/`edit`),
ни в notify/alert-сообщениях. `parse_mode: HTML` сохранён → ссылка остаётся кликабельной.
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
## Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089)
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
(эпик ORCH-088): гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя
(`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти
два человеческих решения** — выборочно (лейбл Plane на задаче), декларативно,
обратимо, **не трогая ни одной технической проверки**. Инвариант: авто-режим снимает
лишь ожидание человеческого сигнала; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД
**не трогаются**. Аддитивно: leaf `src/labels.py` (never-raise) + две точечные врезки.
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
`files_ok`): `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
`advance_stage(..., finished_agent=None)`**тот же путь, что человеческий Approved**
(`approved-via-status``analysis → architecture` + `mark_brd_review_ended`).
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` после advance
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b`
(маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
индикативно-человеческие шаги. **BR-5 структурно:** Phase A достигается только после
зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate →
image-freshness → staging) → autoDeploy физически не деплоит сломанное.
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (`None` при ошибке ≠ `[]`) +
`get_project_labels` (`{normalized_name→uuid}`, TTL-кэш); сопоставление по
нормализованному имени (`strip().casefold()`), неоднозначность → «нет лейбла».
Источник истины — Plane API, не payload вебхука. Новый сеттер `set_issue_approved`.
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед.
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
«нет авто» → ручной гейт (never-raise). Прозрачность: лог + Telegram + Plane-коммент +
live-карточка; блок `auto_labels` в `GET /queue`. **Инфра-предусловие:** создать лейблы
`autoApprove`/`autoDeploy` в Plane-проекте ORCH (их отсутствие = ручной режим, fail-safe).
Детали — `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
## Конвенции
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
- Work items: `docs/work-items/<plane-id>/`
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза. **ORCH-52c (ORCH-076):** парсинг frontmatter сведён к единому контракту `src/frontmatter.py` (reader `read_frontmatter_value` — BC; единый парс-примитив `parse_frontmatter`; writer `render/write_frontmatter`; валидатор схемы `validate_schema`/`REQUIRED_FIELDS` — warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`). Пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) читают через ОДНУ точку парсинга; семантика вердиктов и `STAGE_TRANSITIONS`/состав `QG_CHECKS` — 1:1. Формальная спека «стадия → обязательный выход» + обязательная frontmatter-схема — `docs/_standards/HANDOFF_PROTOCOL.md`
## Артефакты задачи (`docs/work-items/<plane-id>/`)
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022).
**Стандарт документов (ORCH-075, ORCH-52b):** структура каждого дока, карта «стадия→агент→документ→гейт→machine-key» и конвенция ADR-naming зафиксированы в `docs/_standards/PIPELINE_DOCS.md` (golden source); копируемые скелеты — в `docs/_templates/`. Перед написанием номерного дока бери скелет из `docs/_templates/` и не меняй имя machine-key frontmatter (регистр чувствителен — иначе гейт упадёт ложно).
## Правила для агентов
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR. Обнови `CHANGELOG.md`.
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR (формат — `docs/_standards/PIPELINE_DOCS.md` §4). Структура номерных доков и шаблоны — `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/`. Обнови `CHANGELOG.md`.
3. Никогда не править артефакты других этапов.
4. Никогда не комментировать ТЗ задним числом — если ТЗ не годится, возвращай в Анализ.
5. Никогда не закрывать задачу самостоятельно — это делает CI / финальная стадия.

View File

@@ -121,6 +121,7 @@ uvicorn src.main:app --reload --port 8500
| `ORCH_REPOS_DIR` | Repos dir (container) | `/repos` |
| `ORCH_HOST_REPOS_DIR` | Repos dir (host) | `/home/slin/repos` |
| `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` |
| `ORCH_RUNS_DIR` | Базовый каталог per-run логов агентов (`<runs_dir>/{run_id}.log`, ORCH-087) | `/app/data/runs` |
| `ORCH_MAX_CONCURRENCY` | Сколько jobs воркер запускает параллельно (ORCH-1) | `1` |
| `ORCH_QUEUE_POLL_INTERVAL` | Период опроса очереди воркером, сек (ORCH-1) | `2.0` |
| `ORCH_PREFLIGHT_CACHE_TTL` | Кэш preflight (CLI/net), сек (ORCH-1 resilience) | `45` |

View File

@@ -0,0 +1,118 @@
# HANDOFF_PROTOCOL — формальный контракт handoff «стадия → обязательный выход»
> **Назначение.** Нормативная спека: что КАЖДАЯ стадия конвейера обязана оставить на выходе —
> какие документы и какие frontmatter-ключи. Дополняет [`PIPELINE_DOCS.md`](PIPELINE_DOCS.md)
> (карта «документ → агент → стадия → гейт → machine-key») «вертикальным» срезом по стадиям и
> вводит **обязательную frontmatter-схему** для машинной проверки.
>
> **Статус истины (важно).** Источник истины поведения — **код**: `src/stages.py`
> (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS` / `check_*` / `_parse_*`),
> `src/stage_engine.py` (врезки под-гейтов). Машинный контракт чтения/записи/валидации
> frontmatter — `src/frontmatter.py`. Эта спека **документирует**; при расхождении первичен код
> (правило ORCH-075).
Введено задачей **ORCH-076** (ORCH-52c — слой 2 эпика ORCH-52: машинный контракт). Слой 1
(ORCH-075/52b) дал описательный стандарт документов; ORCH-52c реализовала единый машинный
frontmatter-контракт (reader + writer + валидатор) и свела чтение пяти вердиктов к одной точке
парсинга. Сквозной ADR: [`adr-0020-frontmatter-contract.md`](../architecture/adr/adr-0020-frontmatter-contract.md);
детально — [`ORCH-076/06-adr/ADR-001-frontmatter-contract.md`](../work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md).
---
## 1. Обязательная frontmatter-схема (машинный источник: `frontmatter.REQUIRED_FIELDS`)
Forward-looking аддитивная схема: набор полей, которые handoff-документ стадии **должен** нести
в ведущем YAML-frontmatter. Машинный источник истины — кортеж
[`src/frontmatter.py`](../../src/frontmatter.py) `REQUIRED_FIELDS`:
| Поле | Смысл |
|------|-------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) — к какой задаче относится выход |
| `stage` | стадия, на выходе которой написан документ (`analysis``deploy`) |
| `author_agent` | роль-автор (`analyst` / `architect` / `developer` / `reviewer` / `tester` / `deployer`) |
| `status` | человеко/машинно-читаемый статус выхода стадии |
| `created_at` | дата создания артефакта (YYYY-MM-DD) |
| `model_used` | модель агента, сгенерировавшего артефакт (`claude-…`) |
**Режим проверки (ORCH-52c, критично для self-hosting).** Валидатор схемы
`frontmatter.validate_schema` / `maybe_warn_schema` по умолчанию **warning-only** и **никогда не
влияет на boolean-вердикт ни одного гейта**: отсутствие полей логируется (`logger.warning`), но не
роняет конвейер и не заваливает гейт. Жёсткий режим (hard-fail) зарезервирован на будущее
(ORCH-52d) и включается ТОЛЬКО kill-switch'ем `frontmatter_validation_strict`
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`). Схема **аддитивна**: старый
документ-вердикт без этих полей читается гейтом ровно как раньше (см. §3).
---
## 2. Контракт handoff по стадиям
Категории документов — как в `PIPELINE_DOCS.md` §2: **required** (всегда), **when-applicable**
(при наличии предмета: инфра / данные / security / post-deploy — отсутствие не нарушение).
«Machine-verdict ключ» — поле, которое exit-гейт/под-гейт ребра читает ТОЛЬКО из frontmatter
(никогда из прозы). Набор документов/ключей/гейтов **согласован 1:1 с `PIPELINE_DOCS.md` §2§3**.
| Стадия (выход) | Агент | Обязательные документы на выходе | Machine-verdict ключ (читает гейт ребра) | Гейт ребра |
|----------------|-------|----------------------------------|------------------------------------------|------------|
| `created` | система (`_create_initial_docs`) / заказчик | `00-business-request.md` | — (вход, не гейтится) | — |
| `analysis` | analyst | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml` | — (гейт проверяет наличие файлов + Approved) | `check_analysis_approved` |
| `architecture` | architect | `06-adr/ADR-NNN-<slug>.md` (≥1); `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md` (when-applicable/required-info) | — (гейт проверяет наличие `06-adr/` ≥1 ИЛИ `07-…`) | `check_architecture_done` |
| `development` | developer | код + тесты в ветке (артефакт-док не пишется; гейт — зелёный CI) | — (гейт читает CI-статус Gitea) | `check_ci_green` |
| `review` | reviewer | `12-review.md` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) | `check_reviewer_verdict` |
| `testing` | tester | `13-test-report.md` | `result:` / `verdict:` / `status:` (`PASS` \| `FAIL` \| `BLOCKED`; три равноранговых, ORCH-047) | `check_tests_passed` |
| `deploy-staging` | deployer | `15-staging-log.md` (required для self-hosting); `17-security-report.md` (security-под-гейт, when-applicable) | `staging_status:` (`SUCCESS` \| `FAILED`); `security_status:` (`PASS` \| `FAIL`) | `check_staging_status` (ребро); под-гейты ребра `deploy-staging→deploy`: `check_security_gate``check_branch_mergeable``check_staging_image_fresh` |
| `deploy` | deployer / deploy-finalizer | `14-deploy-log.md` | `deploy_status:` (`SUCCESS` \| `FAILED`) | `check_deploy_status` |
| `done` | — | — (терминал) | — | — |
| пост-`done` наблюдение | post-deploy-monitor | `16-post-deploy-log.md` (when-applicable, ORCH-021) | `post_deploy_status:` (`HEALTHY` \| `DEGRADED`) — **информационный, не гейт** | — (телеметрия петли уроков / наблюдаемость) |
### Примечания (нормативные)
- **Под-гейты ребра `deploy-staging → deploy`** (`check_security_gate``check_branch_mergeable`
`check_staging_image_fresh`) — это **врезки в `advance_stage`**, а НЕ строки
`STAGE_TRANSITIONS`. Их порядок и условность раската не меняются этой спекой.
- **`15-staging-log.md`** обязателен только для self-hosting репо (`orchestrator`); для прочих
репо staging-гейт — N/A (ORCH-35), документ не требуется.
- **`16-post-deploy-log.md`** несёт `post_deploy_status:`, но это **информационный** ключ
(телеметрия ORCH-8 / наблюдаемость), гейтом он НЕ парсится.
- **`09-…` / `05-…` / `11-…`** — зарезервированные/legacy номера; канон reviewer'а`12-review.md`.
---
## 3. Machine-verdict доки vs информационные (честный механизм проверки)
Полностью согласовано с `PIPELINE_DOCS.md` §3. Machine-verdict док — гейт читает ТОЛЬКО
YAML-frontmatter (через единый `frontmatter.parse_frontmatter`), маппит ключ в вердикт; имя ключа
чувствительно к регистру, значение парсер приводит к верхнему регистру.
| Документ | Machine-key | Парсер | Эффект вердикта |
|----------|-------------|--------|-----------------|
| `12-review.md` | `verdict:` | `check_reviewer_verdict` | `APPROVED` → дальше; `REQUEST_CHANGES` → откат на `development` |
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` | `PASS` → дальше; `FAIL`/`BLOCKED` → откат (негативный токен авторитетен) |
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` | `SUCCESS``done`; `FAILED` → откат (БАГ-8) |
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` | `SUCCESS` → дальше; `FAILED` → откат (self-hosting; иначе N/A) |
| `17-security-report.md` | `security_status:` | `check_security_gate``parse_security_status` | `PASS` → дальше; `FAIL` → откат |
**Информационные доки** (гейтом НЕ парсятся): `00-business-request.md`, `08-data-requirements.md`,
`10-tech-risks.md`, `16-post-deploy-log.md`.
**Аддитивность схемы (§1).** Документ-вердикт БЕЗ полей схемы из §1, но с вердикт-ключом, читается
гейтом РОВНО как раньше: схема не участвует в вычислении вердикта при дефолтном
`frontmatter_validation_strict=False`.
---
## 4. Единый машинный контракт — `src/frontmatter.py`
Все операции с frontmatter сведены в один leaf-модуль (never-raise):
- `read_frontmatter_value(path, key) -> str | None` — single-key reader (контракт неизменен, BC).
- `parse_frontmatter(content) -> FrontmatterParse`**единственная точка** парсинга YAML-frontmatter
(`data` / `has_block` / `malformed` / `yaml_error`); пять вердикт-парсеров делегируют сюда.
- `parse_frontmatter_dict` / `read_frontmatter` — ярлыки к распарсенному mapping.
- `render_frontmatter` / `write_frontmatter` — writer (формат совместим с существующими парсерами).
- `validate_schema` / `REQUIRED_FIELDS` / `maybe_warn_schema` — схема §1 (warning-only по умолчанию).
- `strip_frontmatter` — общий хелпер тела (заменил дубли).
- Kill-switch жёсткой валидации: `config.frontmatter_validation_strict`
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`).
> Перед написанием номерного дока бери скелет из [`docs/_templates/`](../_templates/) и **не меняй
> имя machine-key frontmatter** (регистр чувствителен — иначе гейт упадёт ложно).

View File

@@ -0,0 +1,153 @@
# PIPELINE_DOCS — стандарт документов конвейера (golden source структуры)
> **Назначение.** Единая карта «стадия → агент → документ → категория → гейт/механизм →
> frontmatter machine-key» + конвенция ADR-naming. Это **golden source структуры** номерных
> документов work item (`00-business-request.md` … `17-security-report.md`), который каждая
> агентская роль пишет на своей стадии.
>
> **Статус истины (важно).** Манифест **документирует** текущее поведение гейтов, но НЕ является
> их источником истины. Источник истины — код: `src/stages.py` (`STAGE_TRANSITIONS`),
> `src/qg/checks.py` (`QG_CHECKS` / `check_*` / `_parse_*`), `src/stage_engine.py`. При будущей
> правке гейта первична правка кода, манифест обновляется следом (ORCH-075 / ADR-001 §D2).
>
> **Копируемые скелеты** каждого документа — в каталоге [`docs/_templates/`](../_templates/):
> «скопировал → заполнил → не угадываешь структуру/ключ».
Введён задачей **ORCH-075** (ORCH-52b — слой 1 эпика ORCH-52). Сквозной ADR:
[`docs/architecture/adr/adr-0019-pipeline-docs-standard.md`](../architecture/adr/adr-0019-pipeline-docs-standard.md);
детально — `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`.
---
## 1. Конвейер стадий (ground-truth `STAGE_TRANSITIONS`)
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
↑ │
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3 retries)
```
Каждое ребро несёт ровно один exit-гейт (`src/stages.py`):
`check_analysis_approved → check_architecture_done → check_ci_green → check_reviewer_verdict →
check_tests_passed → check_staging_status → check_deploy_status`.
**Под-гейты ребра `deploy-staging → deploy`** (`check_security_gate``check_branch_mergeable`
`check_staging_image_fresh`) — это **врезки в `advance_stage`**, а НЕ строки `STAGE_TRANSITIONS`.
Аналогично под-гейт ребра `deploy → done` (`_handle_merge_verify`, ORCH-071/073) — врезка, не
зарегистрированный QG. Карта стадий о них не «лжёт»: они не являются стадиями.
---
## 2. Манифест: документ → агент → категория → стадия → гейт → machine-key
Категории: **required** (пишется всегда), **when-applicable** (пишется при наличии предмета:
инфра / данные / security / post-deploy — отсутствие не нарушение), **optional** / **legacy**.
| Документ | Владелец-агент | Категория | Стадия написания | Гейт / механизм проверки | Frontmatter machine-key |
|----------|----------------|-----------|------------------|--------------------------|-------------------------|
| `00-business-request.md` | система (Plane webhook `_create_initial_docs`) / заказчик | required | `created` (инициализация) | не гейтится (вход) | — |
| `01-brd.md` | analyst | required | `analysis` | exit-гейт `analysis→architecture` = `check_analysis_approved` (Approved + полнота файлов); helper `check_analysis_complete` (наличие `01/02/03/04`) | — |
| `02-trz.md` | analyst | required | `analysis` | то же | — |
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
| `06-adr/ADR-NNN-<slug>.md` | architect | required | `architecture` | `check_architecture_done` (наличие каталога `06-adr/` ≥1 файл ИЛИ `07-infra-requirements.md`) | — |
| `07-infra-requirements.md` | architect | when-applicable | `architecture` | `check_architecture_done` (учитывается при наличии) | — |
| `08-data-requirements.md` | architect | when-applicable | `architecture` | информационный (гейтом не парсится) | — |
| `10-tech-risks.md` | architect | required | `architecture` | информационный (гейтом не парсится) | — |
| `12-review.md` | reviewer | required | `review` | `check_reviewer_verdict` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) |
| `13-test-report.md` | tester | required | `testing` | `check_tests_passed` (`_parse_tests_verdict`) | `result:` / `verdict:` / `status:` (`PASS` \| `FAIL` \| `BLOCKED`; три равноранговых, ORCH-047) |
| `14-deploy-log.md` | deployer / deploy-finalizer | required | `deploy` | `check_deploy_status` (`_parse_deploy_status`) | `deploy_status:` (`SUCCESS` \| `FAILED`) |
| `15-staging-log.md` | deployer | required (self-hosting) | `deploy-staging` | `check_staging_status` (self-hosting; иначе N/A — ORCH-35) | `staging_status:` (`SUCCESS` \| `FAILED`) |
| `16-post-deploy-log.md` | post-deploy-monitor | when-applicable | пост-`done` наблюдение (ORCH-021; не ребро `STAGE_TRANSITIONS`) | информационный (гейтом не парсится) | `post_deploy_status:` (`HEALTHY` \| `DEGRADED`) |
| `17-security-report.md` | security-гейт (детерминированный, ORCH-022) | when-applicable | под-гейт ребра `deploy-staging→deploy` | `check_security_gate` (врезка в `advance_stage`) | `security_status:` (`PASS` \| `FAIL`) |
### Примечания манифеста (нормативные)
- **Под-гейты ребра `deploy-staging→deploy`** (`check_security_gate``check_branch_mergeable`
`check_staging_image_fresh`) исполняются как врезки в `advance_stage`, а НЕ строки
`STAGE_TRANSITIONS`. Не путать с exit-гейтами рёбер.
- **`09-review.md`** — legacy fallback от старой нумерации; **канон — `12-review.md`**. В основную
таблицу как канон не вносится; reviewer пишет `12-review.md`.
- **Категория `when-applicable`** = документ пишется при наличии соответствующего предмета
(инфра / данные / security / post-deploy). Его отсутствие — не нарушение приёмки.
- **`05-…` / `09-…` / `11-…`** — зарезервированные/legacy номера, в текущем каноне не используются.
---
## 3. Machine-verdict доки vs информационные (честный механизм проверки)
**Machine-verdict доки** — гейт читает ТОЛЬКО YAML-frontmatter (никогда прозу), маппит ключ в
вердикт. Имя ключа чувствительно к регистру; значение парсер приводит к верхнему регистру.
| Документ | Machine-key | Парсер (`src/qg/checks.py`) | Эффект вердикта |
|----------|-------------|-----------------------------|-----------------|
| `12-review.md` | `verdict:` | `check_reviewer_verdict` | `APPROVED` → дальше; `REQUEST_CHANGES` → откат на `development` |
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` | `PASS` → дальше; `FAIL`/`BLOCKED` → откат |
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` | `SUCCESS``done`; `FAILED` → откат (БАГ-8) |
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` | `SUCCESS` → дальше; `FAILED` → откат (self-hosting; иначе N/A) |
| `17-security-report.md` | `security_status:` | `check_security_gate` | `PASS` → дальше; `FAIL` → откат |
**Информационные доки** — гейтом НЕ парсятся (структура ничего не блокирует):
`00-business-request.md` (вход), `08-data-requirements.md`, `10-tech-risks.md`,
`16-post-deploy-log.md` (несёт `post_deploy_status:`, но это телеметрия петли уроков ORCH-8 /
наблюдаемость, не гейт).
---
## 4. Конвенция ADR-naming
### Per-work-item ADR (основное)
- **Путь:** `docs/work-items/<plane-id>/06-adr/`
- **Имя файла:** `ADR-NNN-<kebab-slug>.md`
- `NNN` — 3-значный, начинается с `001`; инкремент при нескольких ADR в одной задаче
(`ADR-001-…`, `ADR-002-…`).
- `<kebab-slug>` — kebab-case (нижний регистр, слова через дефис), отражает суть решения.
- **Стадия:** пишет **architect** на стадии `architecture`; гейтится `check_architecture_done`
(наличие каталога `06-adr/` ≥ 1 файла).
### Сквозной (cross-cutting) ADR
Решения, затрагивающие несколько компонентов/ролей или поведение всего конвейера, **дублируются**
в глобальный реестр:
- **Путь:** `docs/architecture/adr/`
- **Имя файла:** `adr-NNNN-<kebab-slug>.md` (4-значная сквозная нумерация, последовательная по
всему репозиторию; на момент ORCH-075 реестр доходит до `adr-0019`).
### Примеры из репозитория (реальные, проверенные)
- `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`
- `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`
- `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`
- Сквозные: `docs/architecture/adr/adr-0017-serial-gate.md`,
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
---
## 5. Как пользоваться шаблонами
1. Скопируй нужный скелет из [`docs/_templates/`](../_templates/) в
`docs/work-items/<plane-id>/` под канонным именем (для ADR — `06-adr/ADR-001-<slug>.md`).
2. Заполни секции; **не удаляй** machine-key frontmatter у machine-verdict доков и **не меняй имя
ключа** (регистр чувствителен — иначе гейт упадёт ложно).
3. Сверяйся с манифестом (§2§3): какой агент, на какой стадии, какой гейт читает документ.
> Стандарт **описательный** (слой 1). **Машинный слой реализован в ORCH-52c (ORCH-076):** единый
> frontmatter-контракт (reader + writer + валидатор) в [`src/frontmatter.py`](../../src/frontmatter.py)
> и формальная спека handoff [`HANDOFF_PROTOCOL.md`](HANDOFF_PROTOCOL.md) («стадия → обязательный
> выход» + обязательная frontmatter-схема `REQUIRED_FIELDS`). Пять вердикт-парсеров
> (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`,
> `parse_security_status`) читают вердикт через ОДНУ точку парсинга (`parse_frontmatter`); семантика
> вердиктов 1:1. Валидатор обязательной схемы по умолчанию **warning-only** (kill-switch
> `frontmatter_validation_strict`, дефолт `False`) — соблюдение схемы пока на ответственности агента
> и reviewer'а, enforcement придёт с ORCH-52d.
---
## 6. Спека handoff (машинный контракт, ORCH-52c)
Вертикальный срез «стадия → обязательные документы + frontmatter-ключи на выходе» и обязательная
frontmatter-схема вынесены в отдельную нормативную спеку [`HANDOFF_PROTOCOL.md`](HANDOFF_PROTOCOL.md)
(набор документов/ключей/гейтов согласован 1:1 с §2§3 этого манифеста). Машинный источник
обязательной схемы — `frontmatter.REQUIRED_FIELDS`.

View File

@@ -0,0 +1,8 @@
# Business Request: <краткий заголовок задачи>
Work Item ID: ORCH-NNN
## Description
<Что хочет заказчик/Владелец своими словами: проблема, желаемый результат, контекст.
Допускается `TBD` на входе — analyst уточняет на стадии `analysis` и формализует в 01-brd.md.>

34
docs/_templates/01-brd.md vendored Normal file
View File

@@ -0,0 +1,34 @@
# 01 — BRD (бизнес-требования): ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
## 1. Бизнес-контекст и проблема
<Зачем задача, какую боль/риск закрывает. Установленные факты — не изобретать.>
## 2. Объём (scope)
### В объёме
- <что делаем>
### Вне объёма
- <что явно НЕ делаем — чтобы исключить расползание>
## 3. Заинтересованные стороны
<Кто заказчик, кого затрагивает, кто принимает результат.>
## 4. Бизнес-требования (BR)
- **BR-1** — <требование, проверяемое>
- **BR-2** — …
## 5. Нефункциональные требования (NFR)
- **NFR-1** — <надёжность / совместимость / обратимость / безопасность>
- **NFR-2** — …
## 6. Допущения и ограничения
<Допущения, на которых стоит решение; внешние ограничения.>
## 7. Критерии успеха
<Резюме; детальные PASS/FAIL — в 03-acceptance-criteria.md.>
## 8. Риски
<Краткий перечень; детали — 10-tech-risks.md (заполняет архитектор).>

30
docs/_templates/02-trz.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# 02 — ТЗ (TRZ): ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное обоснование/решения — задача архитектора (06-adr).
## 1. Сводка изменения
<Что меняется, в одном-двух абзацах.>
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `src/<module>.py` | изменить / создать |
## 3. Функциональные требования
### FR-1 — <название>
<Поведение, контракт, инварианты. Привязать к BR.>
## 4. Изменения API
<Новые/изменённые эндпоинты; либо «Нет.».>
## 5. Изменения схемы БД
<Таблицы/миграции/индексы; либо «Нет.».>
## 6. Требования к новым/изменённым QG checks
<Изменения `QG_CHECKS` / `check_*`; либо «Нет.».>
## 7. Совместимость / регресс
<Обратная совместимость, kill-switch, область раската, обратимость.>

View File

@@ -0,0 +1,31 @@
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам
репозитория.
---
## AC-1 — <краткий заголовок>
**Условие:** <проверяемое условие>
- **PASS:** <что должно быть истинно>
- **FAIL:** <что считается провалом>
---
## AC-2 — <краткий заголовок>
**Условие:** <…>
- **PASS:** <…>
- **FAIL:** <…>
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1 |
| AC-2 | BR-2 / FR-2 |

20
docs/_templates/04-test-plan.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
work_item: ORCH-NNN
title: "<краткое название тест-плана>"
framework: pytest
scope: "<что покрывается тестами; что вне покрытия>"
notes: >
<Свободные заметки: окружение, особенности, что считается регрессом.
Полный регресс tests/ должен оставаться зелёным.>
tests:
- id: TC-01
type: unit # unit | integration
description: "<что проверяет тест>"
module: tests/test_<feature>.py
expected: PASS
- id: TC-02
type: integration
description: "<…>"
module: tests/test_<feature>.py
expected: PASS

43
docs/_templates/06-adr-ADR-NNN-slug.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# ADR-NNN: <Заголовок решения>
> **Шаблон ADR.** Скопируй в `docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md`.
> `NNN` начинается с `001`, инкремент при нескольких ADR в задаче. `<kebab-slug>` — нижний
> регистр, слова через дефис. Сквозное (cross-cutting) решение дополнительно дублируй в
> `docs/architecture/adr/adr-NNNN-<kebab-slug>.md` (4-значная глобальная нумерация).
> См. `docs/_standards/PIPELINE_DOCS.md` §4.
Work Item: **ORCH-NNN** — <короткое описание>
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-NNNN-<slug>.md`** (если решение
кросс-каттинговое; иначе — «N/A, локальное решение задачи»).
## Статус
Proposed <!-- Proposed | Accepted | Superseded by ADR-… -->
## Контекст
<Какую проблему решаем; факты, сверенные с кодом (`src/…`); почему «как есть» не годится.>
## Решение
### Сводка
<Суть выбранного решения в одном-двух абзацах.>
### D1 — <название аспекта решения>
<Конкретное решение по аспекту, инварианты, привязка к FR/AC.>
### D2 — <название аспекта решения>
<…>
## Альтернативы
- **<альтернатива>** — отвергнуто: <почему>.
## Последствия
- **+** <положительный эффект>
- **** <издержка / приятый компромисс + митигейшн>
- **Откат:** <как полностью откатить изменение>
## Ссылки
- BRD: `docs/work-items/ORCH-NNN/01-brd.md`
- TRZ: `docs/work-items/ORCH-NNN/02-trz.md`
- Acceptance: `docs/work-items/ORCH-NNN/03-acceptance-criteria.md`
- Сверено по коду: `src/…`

View File

@@ -0,0 +1,19 @@
# 07 — Инфра-требования: ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
> When-applicable. Если инфраструктура не затрагивается — оставить явные `N/A` по пунктам
> (файл создаётся для аудитопригодности, а не из-за изменения топологии).
## I-1. Топология / окружения
<Контейнеры, порты, сеть, тома, хост; либо `N/A`.>
## I-2. Переменные окружения / секреты
<Новые env-переменные, изменения `.env` / `.env.example`, секреты; либо `N/A`.>
## I-3. Деплой / рестарт
<Требуется ли рестарт прод-контейнера; self-hosting инвариант (не ронять прод вне staging);
либо `N/A`.>
## I-4. CI/CD
<Изменения `.gitea/workflows/`, новые тестовые шаги; либо «без изменений».>

15
docs/_templates/08-data-requirements.md vendored Normal file
View File

@@ -0,0 +1,15 @@
# 08 — Требования к данным: ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
> When-applicable / информационный (гейтом не парсится). Если данные/схема не затрагиваются —
> оставить явные `N/A`.
## Изменения схемы БД
<Новые/изменённые таблицы, индексы, миграции (`init_db`); либо `N/A`.>
## Новые/изменённые сущности
<Поля, колонки, инварианты данных; либо «Нет.».>
## Совместимость данных / миграции
<Аддитивность, идемпотентность миграций, restart-safe, влияние на общую прод-БД; либо `N/A`.>

16
docs/_templates/10-tech-risks.md vendored Normal file
View File

@@ -0,0 +1,16 @@
# 10 — Технические риски: ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | <описание риска> | Низ./Сред./Выс. | Низ./Сред./Выс. | <как снижаем> |
| TR-2 | <…> | | | |
## Сводный вывод
<Доминирующий класс рисков; нужна ли эскалация `arch:major-change` / возврат в анализ;
итоговая оценка остаточного риска для прод-конвейера (self-hosting).>

31
docs/_templates/12-review.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
type: review
work_item_id: ORCH-NNN
verdict: APPROVED # APPROVED | REQUEST_CHANGES (machine-key — читает check_reviewer_verdict)
version: 1
---
# Review ORCH-NNN
> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter (никогда из прозы).
> `APPROVED` → дальше по конвейеру; `REQUEST_CHANGES` → откат на `development`.
## Summary
<Краткая оценка: реализовано ли по ТЗ/ADR, покрытие тестами, обновлена ли документация.>
## Оси проверки
<Корректность, соответствие ADR/инвариантам, тесты, документация, совместимость/регресс.>
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
## Документация
<Обновлена ли документация (README/CLAUDE/CHANGELOG/архитектура) в том же PR. Нет → REQUEST_CHANGES.>

33
docs/_templates/13-test-report.md vendored Normal file
View File

@@ -0,0 +1,33 @@
---
type: test-report
work_item_id: ORCH-NNN
result: PASS # PASS | FAIL | BLOCKED (machine-key — читает _parse_tests_verdict)
---
# Test Report — ORCH-NNN
> Машинный вердикт читается ТОЛЬКО из frontmatter. Канонический ключ — `result:`; равнорангово
> допускаются `verdict:` / `status:` (ORCH-047). Любой негативный токен (`FAIL`/`BLOCKED`) —
> авторитетен.
## Окружение
- Python: <версия>
- pytest: <версия>
- Дата: YYYY-MM-DD
- Worktree: `feature/ORCH-NNN-<slug>`
## Результаты
### Полный регресс
<`pytest tests/ -q` — итог (N passed); прод-контейнер не трогается.>
### Профильные сюиты
<Целевые тесты задачи.>
### Сопоставление с тест-планом
| TC ID | Описание | Тест-функция | Результат |
|-------|----------|--------------|-----------|
| TC-01 | <…> | test_… | PASS |
### Сопоставление с критериями приёмки
<AC-1…AC-N — покрыт каким тестом / результат.>

14
docs/_templates/14-deploy-log.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
deploy_status: SUCCESS # SUCCESS | FAILED (machine-key — читает _parse_deploy_status)
work_item: ORCH-NNN
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-NNN
> Машинный вердикт читается ТОЛЬКО из `deploy_status:` во frontmatter.
> `SUCCESS` → `done`; `FAILED` → откат на `development` (БАГ-8).
<Краткое описание деплоя: что выкачено, exit-code хука, кто/что зафиксировало вердикт
(детерминированный finalizer Фаза C, не LLM, для self-hosting).>

20
docs/_templates/15-staging-log.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
staging_status: SUCCESS # SUCCESS | FAILED (machine-key — читает _parse_staging_status)
timestamp: YYYY-MM-DDTHH:MM:SSZ
base_url: http://localhost:8501
---
# Staging Gate Log
> Машинный вердикт читается ТОЛЬКО из `staging_status:` во frontmatter. Реален для self-hosting
> (`orchestrator`); для прочих репо гейт — N/A (ORCH-35). `SUCCESS` → дальше; `FAILED` → откат.
Staging test suite — итог (например: «All REAL pipeline checks passed»). Запуск канонически
внутри контейнера `orchestrator-staging` (8501).
## Results
- **Block A (SMOKE)**: <…>
- **Block B (ACCESS)**: <…>
- **Block C (E2E)**: <…>
REAL failed: <none | перечень>.

21
docs/_templates/16-post-deploy-log.md vendored Normal file
View File

@@ -0,0 +1,21 @@
---
post_deploy_status: HEALTHY # HEALTHY | DEGRADED (информационный, гейтом НЕ парсится — телеметрия ORCH-021)
action_taken: NONE # NONE | ALERT_ONLY | ROLLBACK_OK | ROLLBACK_FAILED
work_item: ORCH-NNN
window_s: 900
checks_total: 0
checks_failed: 0
---
# Post-deploy log — ORCH-NNN
> Пост-`done` наблюдение прода (ORCH-021). НЕ ребро `STAGE_TRANSITIONS`, гейтом не парсится —
> frontmatter машиночитаем для петли уроков ORCH-8 / наблюдаемости.
Окно наблюдения: <window_s>s; опросов всего: <checks_total>, с провалом: <checks_failed>.
## Серия наблюдений
<Краткая серия сигналов health / доли 5xx; классификация HEALTHY/DEGRADED.>
## Решение
<Реакция: для self-hosting всегда `ALERT_ONLY` (ручной approve, тик не откатывает прод).>

26
docs/_templates/17-security-report.md vendored Normal file
View File

@@ -0,0 +1,26 @@
---
security_status: PASS # PASS | FAIL (machine-key — читает check_security_gate)
work_item: ORCH-NNN
secrets_found: 0
deps_blocking: 0
deps_warning: 0
deps_audit_degraded: false
---
# Security Report — ORCH-NNN
> Детерминированный security-гейт (ORCH-022) — под-гейт ребра `deploy-staging→deploy` (врезка в
> `advance_stage`, не строка `STAGE_TRANSITIONS`). Машинный вердикт читается ТОЛЬКО из
> `security_status:`. `PASS` → дальше; `FAIL` → откат.
## Verdict
<clean / blocking: N secrets, M blocking CVE(s).>
## Secrets
<secret-scanning (gitleaks, offline): None | перечень.>
## Dependencies (blocking)
<dependency audit (pip-audit): None | перечень блокирующих CVE.>
## Dependencies (warning)
<Не блокирующие предупреждения зависимостей.>

View File

@@ -13,7 +13,7 @@
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`.
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max``queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
- **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.
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7 и [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md).
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость).
@@ -39,7 +39,23 @@ created → analysis → architecture → development → review → testing →
**Реестр 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`.
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. **Единый frontmatter-контракт (ORCH-52c / ORCH-076):** парсинг YAML-frontmatter сведён к одной точке — `src/frontmatter.parse_frontmatter` (структура `data/has_block/malformed/yaml_error`, never-raise); пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) делегируют ей вместо дублированной ad-hoc логики. Модуль также несёт writer (`render/write_frontmatter`), валидатор обязательной схемы (`validate_schema`/`REQUIRED_FIELDS`, warning-only по умолчанию; hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`) и общий `strip_frontmatter`. Семантика вердиктов / `STAGE_TRANSITIONS` / состав `QG_CHECKS` — без изменений (1:1).
### Стандарт документов конвейера (ORCH-075, ORCH-52b)
Структура номерных документов work item (`00-business-request.md``17-security-report.md`),
карта «стадия → агент → документ → категория → гейт/механизм → frontmatter machine-key» и
конвенция ADR-naming зафиксированы как golden source в
[`docs/_standards/PIPELINE_DOCS.md`](../_standards/PIPELINE_DOCS.md); копируемые скелеты — в
[`docs/_templates/`](../_templates/). Манифест **документирует** поведение гейтов (источник истины
остаётся код: `src/stages.py`, `src/qg/checks.py`), честно различая machine-verdict доки
(`12/13/14/15/17` — несут читаемый гейтом ключ) и информационные (`00/08/10/16` — гейтом не
парсятся). Это слой 1 (описательный). **Слой 2 (машинный) реализован в ORCH-52c (ORCH-076):**
единый frontmatter-контракт `src/frontmatter.py` + формальная спека handoff «стадия → обязательный
выход» с обязательной frontmatter-схемой (`REQUIRED_FIELDS`) —
[`docs/_standards/HANDOFF_PROTOCOL.md`](../_standards/HANDOFF_PROTOCOL.md). ADR:
[adr-0019](adr/adr-0019-pipeline-docs-standard.md) / [adr-0020](adr/adr-0020-frontmatter-contract.md),
детально — `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`,
`docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`.
### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74)
Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
@@ -92,6 +108,84 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch
Подробнее: [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md), детально — `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`.
### Per-repo serial gate: пакетный автономный режим (ORCH-088 — реализовано)
Эпик «1020 задач за ночь», Этап 1 (serial e2e). Закрывает **stale-анализ**: ветка задачи N+1
срезалась на входе в анализ (`start_pipeline._create_gitea_branch`) от `main`, ещё не содержащего код
предшественника N (физическое код-затирание уже закрыто ORCH-026; ORCH-088 — **логический** разрыв).
Новая задача репо не входит в `analysis` (не режет ветку, не запускает analyst), пока в том же репо
есть незавершённая задача (`stage != 'done'`) или репо заморожен. Аддитивно, под kill-switch, область
репо, never-raise, restart-safe; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*`**без изменений**.
- **Gate-в-claim** (`db.claim_next_job`) — analyst-job (`jobs.agent='analyst'`) применимого репо не
выбирается, если `EXISTS` **более ранняя** незавершённая задача репо (`t2.id < jobs.task_id`) ИЛИ
активна строка `repo_freeze`. По образцу `task_deps` `NOT EXISTS` (ORCH-026); только локальная БД
(offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. **FIFO-уточнение реализации
(FR-2):** ADR-001 D1 фиксировал псевдо-SQL `t2.id != jobs.task_id`; при `!=` пакет одновременно
созданных свежих задач (все в `analysis`) взаимно блокировался бы (каждая — «другая незавершённая»
для остальных) ⇒ дедлок всей serial-очереди. `<` допускает ровно самую раннюю задачу и сериализует
остальные за ней (строго по одной, FIFO по `jobs.id`), при этом по-прежнему не блокирует rework-analyst
собственной задачи (R-7) и сохраняет AC-1.
- **Отложенный срез ветки (анти-stale-base, AC-6):** для применимого репо `start_pipeline` создаёт
task-row + enqueue analyst, но **не** создаёт Gitea-ветку/docs; срез релоцируется на момент claim
analyst-job (launcher), когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main,
ORCH-071/073). `ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно
(`_create_gitea_branch` 409 = no-op).
- **Durable per-repo freeze** (новая аддитивная таблица `repo_freeze`, `cleared_at IS NULL` = активен) —
post-deploy `DEGRADED`/rollback (ORCH-021) → `set_repo_freeze` + Telegram-алерт; gate закрыт
безусловно до **ручного** снятия (`POST /serial-gate/unfreeze`). Деградировавшая задача уже `done`
(BR-7) ⇒ отдельный сигнал, независимый от `stage`.
- **Согласование NFR-1:** hot-claim тотальный сбой построения gate-фрагмента → **fail-open** (не
заклинить очередь всех проектов, AC-8); freeze в Python-слое (`is_repo_frozen`) → **fail-closed**
(безопасность прода, AC-9).
- Чистая логика — leaf `src/serial_gate.py` (never-raise). Флаги `serial_gate_enabled` (kill-switch),
`serial_gate_repos` (CSV; **пусто ⇒ все репо**, в отличие от self-hosting-only ORCH-35/43/58),
`serial_gate_freeze_enabled`. Наблюдаемость — аддитивный блок `serial_gate` в `GET /queue`
(per-repo `active_task` / `waiting` / `frozen`). Cross-repo параллелизм сохранён (FR-3); при
выключенном флаге — нулевая регрессия (enduro не затронут).
Подробнее: [adr-0017](adr/adr-0017-serial-gate.md), детально —
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`,
`docs/work-items/ORCH-088/08-data-requirements.md`.
### Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089 — реализовано)
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон (эпик
ORCH-088): гейт BRD (`analysis`: ждёт ручного `Approved`) и гейт прод-деплоя (`deploy`:
Phase A ждёт ручного `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти два
человеческих решения** — выборочно (лейбл Plane на задаче), декларативно, обратимо, **не
трогая ни одной технической проверки**. Аддитивно, по образцу условных под-гейтов
(ORCH-035/043/058/059/088): leaf `src/labels.py` (never-raise) + точечные врезки + флаги;
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД — **не трогаются**.
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
`files_ok`) после `In Review`+коммента: `set_issue_approved` (индикация) +
лог/Telegram/Plane-коммент + `advance_stage(..., finished_agent=None)` — **тот же путь, что
человеческий Approved** (`approved-via-status``analysis → architecture` +
`mark_brd_review_ended`). Без дублирования переходной логики.
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` сразу после advance
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)`
(idempotency-маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
индикативно-человеческие шаги (`Awaiting Deploy` + «ask-human»). **BR-5 структурно:** Phase A
достигается только после зелёных под-гейтов ребра `deploy-staging → deploy` (security →
merge-gate → image-freshness → staging) → autoDeploy физически не деплоит сломанное.
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (поле `labels` issue, `None` при
ошибке ≠ `[]`) + `get_project_labels` (`{normalized_name→uuid}`, TTL-кэш по образцу
`get_project_states`); сопоставление по нормализованному имени (`strip().casefold()`),
неоднозначность → «нет лейбла». Источник истины — Plane API, не payload вебхука. Новый
сеттер `set_issue_approved` (ключ `approved` уже в `_DEFAULT_STATES`).
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед,
нулевая регрессия для enduro.
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
«нет авто» → ручной гейт (never-raise). **Идемпотентность:** autoApprove — advance один раз
(поздний Approved/F-2 видят `architecture`); autoDeploy — маркер `INITIATED`. **Прозрачность
(AC-7):** лог + Telegram + Plane-коммент + live-карточка; блок `auto_labels` в `GET /queue`.
- **Инфра-предусловие:** создать лейблы `autoApprove`/`autoDeploy` в Plane-проекте ORCH
(labels API); их отсутствие = `has_label` False = ручной режим (fail-safe).
Подробнее: [adr-0018](adr/adr-0018-auto-label-gates.md), детально —
`docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
`docs/work-items/ORCH-089/07-infra-requirements.md`.
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`,
@@ -206,6 +300,39 @@ merge-в-main вообще**. Detached host-деплой лишь retag'ал о
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`,
`docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
#### Гарантированный код-PR перед merge-verify (ORCH-082 — фикс ложного HOLD «no open PR»)
Под-гейт merge-verify (ORCH-071/073) детерминированно мержит **открытый** код-PR ветки в `main`
(`merge_pr`, фильтр `head.ref==branch` И `base.ref=="main"`). Но конвейер **не гарантировал**, что
к моменту merge у ветки этот PR есть: PR создаётся единственной `launcher._ensure_pr` **только** на
developer-пути и **только** при свежем worktree-коммите. На деплое ORCH-074 (08.06, первая задача
после ручных восстановлений `main`) у ветки не оказалось открытого код-PR → `merge_pr` вернул
`("False", "no open PR")` → защита ORCH-073 верно удержала задачу (HOLD, не ложный `done`), но это
лечило следствие. ORCH-082 закрывает **отсутствующий инвариант** «к merge-verify у ветки есть
открытый код-PR» аддитивно, внутри того же под-гейта, не трогая машину стадий:
- **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise):
`GET …/pulls?state=open` с фильтром `head.ref==branch` И `base.ref=="main"` (**идентичен**
`merge_pr`/ORCH-073 FR-3 — авто-docs-PR `base != main` НЕ код-PR) → `("existed", N)`; иначе
`POST …/pulls``("created", N)`; гонка «PR exists»/409/422 → повторный GET → `existed` (без
дублей); любая иная ошибка → `("failed", reason)`.
- **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и **ПЕРЕД** `merge_pr`:
`created|existed` → штатно к `merge_pr``verify_merged_to_main`; `failed` → честный HOLD+alert
через новый helper `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от
not-merged HOLD; `result.note="pr-create-failed-hold"`), задача остаётся на `deploy`, БЕЗ отката
на development.
- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО
`verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` устраняет лишь
**ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде).
- **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания PR),
сохранив прежний триггер «только developer-путь».
- **Условность как ORCH-35/43/58/71:** kill-switch `merge_verify_autocreate_pr_enabled` (дефолт
`true`); область — `merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); non-self —
no-op. `False` → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), **без
миграции БД** (restart-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push.
Подробнее: [adr-0016](adr/adr-0016-ensure-open-pr-before-merge-verify.md) (amends 0013/0014);
детально — `docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`.
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 —
@@ -301,6 +428,42 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
### Live-трекер: зачистка сирот + эффорт в карточке + честное время (ORCH-087 — реализовано)
Скалярный `tasks.tracker_message_id` (только последний `message_id`) при рассинхроне
bump-режима (доминанты: гонка двух `update_task_tracker` и delete-fail+send-ok)
терял ссылку на прежние карточки → **осиротевшие «замёрзшие»** карточки (скриншот
ORCH-082: `📍 To Analyse` на задаче, реально дошедшей до `deploy`). G0-расследование
([ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md)):
рендер исправен, корень — потеря учёта старых mid. Решение (bump сохраняется как
дефолт — фича «карточка внизу» ORCH-042/067):
- **G1 — полный учёт mid:** аддитивная таблица-леджер `tracker_messages(task_id,
message_id, created_at, deleted_at)` (вариант A1; JSON-массив A2 отклонён —
lost-update при гонке). На каждом bump зачищаются ВСЕ незакрытые mid (`deleted_at
IS NULL`): успех/«already gone» → `deleted_at`, transient → остаётся для ретрая;
новый mid в леджер + `set_tracker_message_id` ТОЛЬКО при `send is not None` (BR-6).
Скаляр `tracker_message_id` сохранён (BC). Остаточная гонка самозалечивается за один
переход (лок не вводится). Known-limitation: Telegram 48ч (сироты старше неудаляемы).
- **G2/G3 — заголовок/deploy-цикл:** после G1 единственная живая карточка несёт
заголовок текущей стадии; `_LIVE_BRANCH_LABELS` дополняется ключом `confirm_deploy`
(полнота цикла `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`).
- **BR-EFF — эффорт в строке стадии:** новая колонка `agent_runs.effort TEXT`,
стамп фактического `resolve_agent_effort` в `launcher._spawn` (CLI эффорт не
возвращает); рендер `· {model} · {effort}` (developer=`xhigh`, tester/deployer=
`medium`, прочие=`high`); пустой → суффикс опускается.
- **BR-G5 — честное время:** done-строка `⏱️ Агенты {agent} · твоё {review~cap} ·
общее с ожиданием {wall}` — три независимых подписанных метрики; `agent`=Σ
`agent_runs` (главная, точная); «твоё» ограничено порогом
`tracker_brd_review_cap_s` (дефолт 2ч, маркер `~` при отсечке аномального застоя);
`wall` подписан «с ожиданием», не выдаётся за сумму.
- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/стадии — без изменений; миграции
аддитивны/идемпотентны (общая прод-БД, enduro не трогается); never-raise,
`disable_notification`, `plane_issue_link` (ORCH-067), `disable_web_page_preview`
(ORCH-080) — сохранены; разработка поверх свежего `origin/main` (ORCH-86),
`reconciler.py` не эродируется.
Детально — [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md),
`docs/work-items/ORCH-087/08-data-requirements.md`.
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
@@ -318,6 +481,18 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
ET-013) и (2) в явном Plane-статусе **Blocked** / **Needs Input** (Вариант A —
запрос Plane API, без миграции БД; never-raise → консервативный skip). Гард
retry-count проверяется первым (дёшево, локальный SQL).
**ORCH-086 (закрытие F-1-пробела ORCH-068):** терминал-исключение и `state_uuid`-dedup
(изначально только F-2) распространены на F-1. После дешёвых локальных гардов F-1 делает
**один** резолв Plane-статуса задачи на тик (общий fetch для Guard 2 + терминал-скипа +
`_note_unblock`); терминальная задача (группа Plane `completed`/`cancelled`, fallback —
логические ключи `done`/`cancelled`, ЛИБО стадия в БД орка ∈ `{done, cancelled}`) →
**безусловный** ранний скип (`skipped_terminal_total++`, без `advance`/уведомления; не подчинён
`reconcile_skip_blocked_enabled`). Вызов `_note_unblock` на F-1 теперь передаёт `state_uuid` →
in-memory dedup работает на обоих путях (страховка от повтора после рестарта). Лечит
периодическое ложное «ET-002 done разблокирована (потерян webhook)» для терминальных в Plane
задач (enduro/orchestrator), сохраняя легитимный unblock реально застрявшей не-терминальной
задачи. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/сигнатуры/новые флаги — без изменений. Детали —
`docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md`.
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
**ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane
@@ -471,7 +646,7 @@ Monitoring after Deploy → Done
```
- **Длительность** считается launcher'ом (`_monitor_agent`) и пробрасывается в `_post_usage_comments`; для analyst (коммент строится в `stage_engine`) используется DB-фоллбэк `usage.get_agent_duration(task_id, agent)`.
- **Vердикт-парсер** — `src/frontmatter.read_frontmatter_value(...)` (defensive, никогда не raise). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
- **Vердикт-парсер** — единый контракт `src/frontmatter.py` (defensive, never-raise): коммент-хелпер использует `read_frontmatter_value(...)` (single-key, BC), гейты — `parse_frontmatter(...)` (ORCH-52c). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
- Формат коммента **не** меняет реестр гейтов и стадий; коммент — отображение, не управление.
## База данных (SQLite)
@@ -480,6 +655,7 @@ Monitoring after Deploy → Done
- `agent_runs` — запуски агентов (run_id, usage, cost)
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
- `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A»
- `repo_freeze` — durable per-repo rollback-freeze (ORCH-088, FR-5): `(id, repo, frozen_at, reason, work_item_id, cleared_at)`, аддитивная append-only; активный freeze ⇔ строка репо с `cleared_at IS NULL`. Выставляется post-deploy `DEGRADED` (`set_repo_freeze`), снимается вручную (`POST /serial-gate/unfreeze` → `cleared_at=now`). Гейтит serial-claim безусловно (деградировавшая задача уже `done`)
## Изоляция (git worktree, ORCH-2)
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
@@ -489,7 +665,8 @@ Monitoring after Deploy → Done
|--------|------|----------|
| GET | `/health` | health check |
| GET | `/status` | активные задачи (stage != done) |
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + последние jobs |
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + последние jobs |
| POST | `/serial-gate/unfreeze` | ORCH-088 (FR-5): ручное снятие per-repo rollback-freeze (query/body `repo=<repo>`) → `{ok, repo, cleared, frozen}`; идемпотентно. Альтернатива — `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` |
| POST | `/webhook/plane` | Plane webhook |
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
@@ -507,3 +684,4 @@ Monitoring after Deploy → Done
*Актуально на 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`).*
*Актуально на 2026-06-09. Статус доработки: ORCH-088 (per-repo serial gate, Этап 1 serial e2e, adr-0017, `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`) — реализовано в ветке feature/ORCH-088 (leaf src/serial_gate.py never-raise: gate-фрагмент в src/db.py `claim_next_job` fail-OPEN c FIFO-условием `t2.id < jobs.task_id` + freeze `repo_freeze.cleared_at IS NULL`, freeze-решения fail-CLOSED; отложенный срез ветки src/webhooks/plane.py `start_pipeline` → src/agents/launcher.py `_materialize_deferred_branch` (sync `asyncio.run` в worker-потоке) при claim analyst-job; durable freeze таблица `repo_freeze` (idempotent миграция в init_db) + `set_repo_freeze` в src/stage_engine.py DEGRADED-ветке `run_post_deploy_monitor` + ручное снятие `POST /serial-gate/unfreeze` в src/main.py; флаги `serial_gate_enabled`/`serial_gate_repos`/`serial_gate_freeze_enabled` в src/config.py; блок `serial_gate` в `GET /queue`; `STAGE_TRANSITIONS`/`QG_CHECKS` НЕ трогаются; обновлять также при изменении этих мест).*

View File

@@ -21,12 +21,19 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
| adr-0013 | Merge-в-main + пост-деплой верификация как условие `done` | accepted | 2026-06-08 | ORCH-071 |
| adr-0014 | SHA-в-main — единственный критерий merge-verify + регресс-гард | accepted | 2026-06-08 | ORCH-073 |
| adr-0015 | Зависимости задач (B ждёт A) + сериализация merge внутри репо | accepted | 2026-06-08 | ORCH-026 |
| adr-0016 | ensure_open_pr — гарантированный код-PR перед merge-verify | accepted | 2026-06-09 | ORCH-082 |
| adr-0017 | Per-repo serial gate (пакетный автономный режим, serial e2e) | proposed | 2026-06-09 | ORCH-088 |
| adr-0018 | Авто-режим по лейблам (autoApprove + autoDeploy) | accepted | 2026-06-09 | ORCH-089 |
| adr-0019 | Стандарт документов конвейера (PIPELINE_DOCS, слой 1) | accepted | 2026-06-09 | ORCH-075 |
| adr-0020 | Единый frontmatter-контракт + спека handoff (reader/writer/валидатор) | accepted | 2026-06-09 | ORCH-076 |
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
> свободный номер (текущий максимум — `0015`).
> свободный номер (текущий максимум — `0020`).
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
> adr-0016 **amends** adr-0013/0014 (гарантирует открытый код-PR перед merge_pr, ORCH-082).
> adr-0020 реализует машинный слой к adr-0019 (ORCH-52b→52c).
## Формат
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.

View File

@@ -0,0 +1,52 @@
# ADR-0016: ensure_open_pr — гарантированный код-PR перед merge-verify (ORCH-082)
## Статус
Accepted — амендмент к [adr-0013](adr-0013-merge-verify-gate.md) и
[adr-0014](adr-0014-merge-verify-sha-source-of-truth.md). Детально:
`docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`.
## Контекст
Merge-verify (ORCH-071/073) — под-гейт ребра `deploy → done`: детерминированно мержит код-PR в
`main` (`merge_pr`) и подтверждает merge **только** по «SHA-в-main» (`verify_merged_to_main`,
ORCH-073). На деплое ORCH-074 (08.06) `merge_pr` вернул `("False", "no open PR")`: у ветки **не
было** открытого PR с `head==branch` И `base=="main"`. Защита ORCH-073 верно удержала задачу
(HOLD, не ложный `done`), но это лечило **следствие**.
Первопричина (код-аудит): PR создаётся в конвейере **единственной** функцией
`launcher._ensure_pr`, вызываемой **только** на developer-пути и **только** при свежем
worktree-коммите. Любой сценарий без свежего developer-коммита (бойнс без правок, повторный
прогон, **ручное восстановление ветки/`main`** — случай ORCH-074) оставляет ветку без код-PR.
Инвариант «к merge-verify у ветки есть открытый код-PR» в конвейере **отсутствовал** → блокер
автономного деплоя (ORCH-54).
## Решение
Аддитивно обеспечить инвариант **внутри того же под-гейта**, ПЕРЕД `merge_pr`, не трогая машину
стадий:
1. **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise):
`GET …/pulls?state=open` с фильтром **`head.ref==branch` И `base.ref=="main"`** (идентичен
`merge_pr`/ORCH-073 FR-3 — авто-docs-PR не считается код-PR) → `("existed", N)`; иначе
`POST …/pulls``("created", N)`; гонка «PR exists» → повторный GET → `existed` (без дублей);
любая ошибка → `("failed", reason)`.
2. **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и ПЕРЕД `merge_pr`:
`created|existed` → штатно к `merge_pr`; `failed` → честный HOLD+alert через новый helper
`_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от not-merged HOLD), задача
остаётся на `deploy`, БЕЗ отката на development.
3. **Kill-switch `merge_verify_autocreate_pr_enabled`** (дефолт `True`); область —
`merge_verify_applies` (self-hosting / `merge_verify_repos`). `False` → поведение ORCH-074 1:1.
4. **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания
PR), сохранив прежний триггер «только developer-путь».
## Последствия
- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО
`verify_merged_to_main` (SHA-в-main) + `check_main_regression`. Создание PR устраняет лишь
**ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде).
- **Без миграций:** идемпотентность выводится из Gitea (наличие открытого PR), схема БД не меняется
— restart-safe; повторный заход (reaper/reconciler/re-approve) → `existed`, дублей нет.
- **Инварианты целы:** `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058) — без изменений; `main` не
push/force-push; never-raise на всём пути.
- **Наблюдаемость:** один однозначный исход в логах на проход — created / existed / failed; HOLD по
failed текстуально отличим от HOLD not-merged.
- **Минус:** код-PR может создаваться после прохождения гейтов — безопасно, т.к. гейты валидируют
код ветки, а merge-verify идёт ПОСЛЕ всех гейтов; PR — лишь механизм слияния, ревью не обходится.

View File

@@ -0,0 +1,59 @@
# adr-0017: Per-repo serial gate (пакетный автономный режим, serial e2e)
Статус: **proposed** · Дата: 2026-06-09 · Источник: **ORCH-088** (Этап 1)
Детально: `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`.
## Контекст
Цель эпика ORCH-088 — масштаб автономности: накидать вечером 1020 задач и получить к утру пакет,
последовательно проведённый через весь конвейер (analysis → … → deploy → done). Корневая проблема —
**stale-анализ**: ветка задачи N+1 срезается на входе в анализ (`start_pipeline._create_gitea_branch`)
от `main`, ещё не содержащего код предшественника N. Физическое код-затирание уже закрыто (ORCH-026
auto_rebase + merge-lease); остаётся **логический** разрыв. Plane API v1 не имеет bulk/relations ⇒
очередь/зависимости хранятся у оркестратора (gate по локальной БД).
## Решение
**Per-repo serial gate** — новая задача репо не входит в `analysis` (не режет ветку, не запускает
analyst), пока в том же репо есть незавершённая задача (`stage != 'done'`) или репо заморожен.
Три механизма, аддитивно, под kill-switch, с областью репо, never-raise, restart-safe:
1. **Gate-в-claim** (`db.claim_next_job`) — analyst-job (`jobs.agent='analyst'`) применимого репо не
выбирается, если `EXISTS` другая незавершённая задача репо ИЛИ активна строка `repo_freeze`. По
образцу `task_deps` `NOT EXISTS` (ORCH-026); только локальная БД (offline hot-path). Job'ы уже
активной задачи проходят свободно; rework-analyst не блокирует себя (`t2.id != jobs.task_id`).
2. **Отложенный срез ветки** — для применимого репо `start_pipeline` создаёт task-row + enqueue
analyst, но **не** создаёт Gitea-ветку/docs; срез релоцируется на момент claim analyst-job
(launcher), когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main, ORCH-071/073).
`ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно (409 = no-op).
3. **Durable per-repo freeze** (`repo_freeze`) — post-deploy `DEGRADED`/rollback (ORCH-021) →
`set_repo_freeze` + Telegram-алерт; gate закрыт безусловно до **ручного** снятия
(`POST /serial-gate/unfreeze`). Деградировавшая задача уже `done` (BR-7) ⇒ нужен отдельный сигнал.
Чистая логика — leaf `src/serial_gate.py` (never-raise). Флаги `serial_gate_enabled` (kill-switch),
`serial_gate_repos` (CSV; **пусто ⇒ все репо**, в отличие от self-hosting-only ORCH-35/43/58),
`serial_gate_freeze_enabled`. Наблюдаемость — блок `serial_gate` в `GET /queue`.
## Альтернативы
- **Гейт в `start_pipeline` + re-trigger при `done`** — больше состояния/путей, риск зависших задач;
relocation на claim переиспользует restart-safe `jobs`-очередь.
- **Freeze как колонка `tasks`** — неверная семантика (freeze per-repo, задача уже `done`).
- **Self-hosting-only область** — лишает enduro анти-stale-base (FR-3).
- **Отдельная таблица очереди ожидания** — избыточно; `jobs(queued)`+gate достаточно.
- **Снятие freeze Plane-жестом** — перегрузка статусов (анти-паттерн ORCH-059).
## Последствия
- **+** AC-6 закрыт структурно; AC-2/AC-3 «бесплатны» (ожидание = `queued` job без ветки);
переиспользование проверенных паттернов; cross-repo параллелизм сохранён; `STAGE_TRANSITIONS` /
`QG_CHECKS` / `check_*` / merge-gate / merge-verify / image-freshness / post-deploy / deploy-хук /
`max_concurrency`**без изменений**.
- **NFR-1:** hot-claim тотальный сбой → **fail-open** (не заклинить очередь всех проектов); freeze в
Python-слое → **fail-closed** (безопасность прода).
- **** Срез ветки/docs мигрируют из async в sync-путь launcher (обёртка); Blocked-задача держит пакет
(Этап 1, осознанно); freeze снимается только вручную.
- Откат: `serial_gate_enabled=False` ⇒ claim/старт 1:1 как до ORCH-088; таблица `repo_freeze` инертна.
- **Вне скопа** (Этап 1): merge-очередь FIFO, pre-merge rebase как отдельная фича, фазы A/B/C,
любой параллелизм задач внутри одного репо, зависимость от ORCH-83.
## Связи
- Переиспользует: adr-0002 (очередь ORCH-1), adr-0015 (claim-gate/auto_rebase/merge-lease ORCH-026),
adr-0010 (post-deploy monitor — источник DEGRADED), adr-0013/0014 (merge-verify ⇒ `done`⇔SHA-в-main).
- Новая аддитивная таблица `repo_freeze` (`docs/work-items/ORCH-088/08-data-requirements.md`).

View File

@@ -0,0 +1,59 @@
# ADR-0018: Авто-режим по лейблам — autoApprove / autoDeploy (ORCH-089)
## Статус
Accepted (реализация — ORCH-089)
## Контекст
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
(эпик ORCH-088, «1020 задач за ночь»):
1. **BRD** (`analysis`): ждёт ручного Plane-статуса `Approved` → advance на `architecture`.
2. **Прод-деплой** (`deploy`): Phase A ставит `Awaiting Deploy` и ждёт ручного
`Confirm Deploy` (ORCH-059) → Phase B (`initiate_deploy`).
Для доверенных задач оба клика избыточны. Нужно снять **только эти два человеческих
решения**, выборочно/декларативно (лейбл Plane на задаче), не ослабляя ни одной
технической проверки.
## Решение
Аддитивно, по образцу условных под-гейтов (ORCH-035/043/058/059/088): leaf-модуль чистой
логики `src/labels.py` (never-raise) + точечные врезки + флаги. `STAGE_TRANSITIONS`, реестр
`QG_CHECKS`, все `check_*`, схема БД — **не трогаются**.
- **`autoApprove`** (лейбл задачи) → в `_handle_analysis_approved_flow` (ветка `files_ok`)
после `In Review`+коммента: `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
`advance_stage(..., finished_agent=None)` — тот же путь, что человеческий Approved
(`approved-via-status``analysis → architecture` + `mark_brd_review_ended`). Без
дублирования переходной логики.
- **`autoDeploy`** (лейбл задачи) → в `_handle_self_deploy_phase_a` сразу после advance на
`deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)`
(idempotency-маркер `INITIATED`, `Deploying`, finalizer). Пропускаются лишь
индикативно-человеческие шаги (`Awaiting Deploy` + «ask-human»).
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` + `get_project_labels` (TTL-кэш,
образец `get_project_states`); сопоставление по нормализованному имени; источник истины —
Plane API (не payload). Новый сеттер `set_issue_approved` (ключ `approved` уже в states).
- **Флаги:** `auto_label_enabled` (kill-switch), `auto_approve_label`/`auto_deploy_label`
(имена), `auto_label_repos` (CSV; **пусто → self-hosting only**), `auto_label_states_ttl_s`.
`applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label` (сеть) — только если
`applies==True` → при выключенном флаге нулевой сетевой оверхед.
## Критические инварианты
- **Авто-режим снимает ТОЛЬКО человеческое решение**, не ослабляя ни один тех-гейт
(CI / staging / security / merge-gate / image-freshness / merge-verify / regression-guard /
post-deploy). autoDeploy живёт в точке, где все под-гейты ребра `deploy-staging → deploy`
уже зелёные → структурно «никогда не деплоит сломанное».
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность имени →
«нет авто» → ручной гейт (согласовано с fail-closed-практикой ORCH-059). never-raise.
- **Нулевая регрессия:** без лейблов / `auto_label_enabled=False` / репо вне scope →
поведение 1:1 как до ORCH-089 (enduro не затронут).
- **Идемпотентность:** autoApprove — advance применяется один раз (поздний Approved/F-2
видят уже `architecture`); autoDeploy — маркер `INITIATED`.
## Последствия
**+** минимальная поверхность, единый источник истины перехода, декларативно/обратимо,
независимые лейблы, безопасный дефолт. **** Approved-статус транзиентен (durable-аудит —
лог/Telegram/коммент); 12 GET к Plane на гейт применимого репо (TTL-кэш карты лейблов);
требуется однократно создать лейблы в Plane-проекте ORCH (инфра-предусловие; их отсутствие =
fail-safe ручной режим).
Детально: `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
`07-infra-requirements.md`, `10-tech-risks.md`.

View File

@@ -0,0 +1,49 @@
# adr-0019: Стандарт документов пайплайна (docs/_standards + docs/_templates + ADR-naming)
Статус: **proposed** · Дата: 2026-06-09 · Источник: **ORCH-075** (ORCH-52b, слой 1 эпика ORCH-52)
Детально: `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`.
## Контекст
Агенты всех ролей пишут номерные доки work item (`00…17`) «по памяти»; каталогов
`docs/_standards/` и `docs/_templates/` нет. Следствия: разнобой структуры между задачами; риск
рассинхрона критичных frontmatter-ключей машинных доков (`verdict:` / `result:` / `deploy_status:` /
`staging_status:` / `security_status:`), которые читает гейт; отсутствует целостная карта «стадия →
агент → документ → гейт». Эпик ORCH-52 слоист: слой 1 (52b) фиксирует **договорённость**, машинная
проверка/валидатор — отдельный слой 52c.
## Решение
**Документационный стандарт, docs-only, выведенный из фактического кода и эталонных доков:**
1. `docs/_standards/PIPELINE_DOCS.md` — манифест-карта «стадия → документ → владелец-агент →
категория (`required`/`when-applicable`/`optional`) → гейт/механизм → frontmatter machine-key».
Манифест **документирует** поведение гейтов (источник истины остаётся `src/`), честно различает
machine-verdict доки (`12,13,14,15,17`) и информационные (`00,08,10,16`), и помечает под-гейты
ребра `deploy-staging→deploy` (security/merge/image-freshness) как врезки в `advance_stage`, а не
строки `STAGE_TRANSITIONS`.
2. `docs/_templates/*` — копируемые скелеты для каждого `required`/`when-applicable` дока; секции
выведены из эталонов (ORCH-088/073/089/071), новые не изобретаются; машинные доки несут точный
frontmatter-ключ из ground-truth.
3. **ADR-naming** канонизирован: `docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md` (NNN с
`001`); кросс-каттинговые решения дублируются в этот глобальный реестр `adr-NNNN-<slug>.md`.
Подключение — ссылки из `CLAUDE.md` и `docs/architecture/README.md` + запись в `CHANGELOG.md`.
## Альтернативы
- Сразу валидатор на гейте — отвергнуто (ORCH-52c; нарушил бы docs-only/NFR-1, групповой риск).
- Манифест как источник истины гейтов — отвергнуто (дубль-истина «манифест ≠ код»).
- Шаблоны в `docs/work-items/_template/` — отвергнуто (риск для сканеров/гейтов наличия файлов).
- Ретро-фит истории доков — отвергнуто (вне scope, отдельный риск).
## Последствия
- **+** Единый golden source структуры доков; меньше ложных падений гейтов из-за неверного
frontmatter-ключа; ADR-naming записан; база для ORCH-52c.
- **+ Нулевой рантайм-риск:** только `docs/**` + `CLAUDE.md` + `CHANGELOG.md`; `STAGE_TRANSITIONS` /
`QG_CHECKS` / `check_*` / `src/stage_engine.py` / схема БД — без изменений; полностью обратимо.
- **** Манифест — снимок поведения гейтов, дрейфует до ORCH-52c (митигейшн: источник истины — код,
reviewer-правило, привязка к именам `check_*`); стандарт описательный, не принуждающий.
## Связи
- Источник: ORCH-075 (`docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`).
- Документирует (не меняет): adr-0003/0006/0008/0012/0013/0014/0016 (гейты и под-гейты ребра),
`STAGE_TRANSITIONS` (`src/stages.py`), `QG_CHECKS` (`src/qg/checks.py`).
- Downstream: ORCH-52c (frontmatter-валидатор / writer-контракт), ORCH-52d (правка промптов).

View File

@@ -0,0 +1,63 @@
# adr-0020: Единый frontmatter-контракт + спека handoff (reader/writer/валидатор)
Статус: **Accepted** · Дата: 2026-06-09 · Источник: **ORCH-076** (ORCH-52c)
Детально: [`docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`](../../work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md)
## Контекст
Слой 1 эпика ORCH-52 (ORCH-075/52b) дал **описательный** стандарт документов
(`docs/_standards/PIPELINE_DOCS.md`), явно отложив машинную проверку на ORCH-52c. В коде:
`src/frontmatter.py` — только single-key reader (never-raise), а ~10-строчный блок парсинга
YAML-frontmatter **продублирован** в 5 вердикт-парсерах (`check_reviewer_verdict`,
`_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`)
+ в `_strip_frontmatter`/`extract_security_findings`. Единого контракта чтения, writer'а, схемы
и формальной спеки handoff — нет. Эти парсеры читают вердикты **на гейтах self-hosting**
инструмента, обслуживающего прод других проектов из общего инстанса → любой регресс = стоп
конвейера всех проектов.
## Решение
1. **`src/frontmatter.py` → полный frontmatter-контракт** (функции в существующем leaf-модуле,
контракт **never-raise**): сохранённый `read_frontmatter_value` (без изменений) + единый
парс-примитив `parse_frontmatter(content) -> FrontmatterParse` (единственная точка
YAML-логики, структура различает no-block / malformed / yaml-error / data) + `render_/
write_frontmatter` (writer) + `validate_schema` (обязательная схема
`work_item, stage, author_agent, status, created_at, model_used`) + `strip_frontmatter`.
2. **Унифицируется механизм парсинга, НЕ семантика.** Все 5 вердикт-парсеров читают YAML через
`parse_frontmatter`; token-наборы, upper-casing, приоритет негативного токена, 3-полевой
контракт tester'а (ORCH-047), fallback `worktree→origin/main`**1:1**. Сигнатуры и
`tuple[bool, str]` — неизменны. Reason-строки переносятся дословно.
3. **Валидатор не hard-fail по умолчанию.** Флаг `frontmatter_validation_strict` (env
`ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`): default — warning/лог, **вне
вердикт-пути гейтов** (нулевая регрессия); hard-fail — зарезервированный strict-режим
(включение — с ORCH-52d). Иначе ORCH-52c заблокировала бы собственный деплой.
4. **Формальная спека handoff** `docs/_standards/HANDOFF_PROTOCOL.md` — «стадия → обязательный
выход» (документы + frontmatter-ключи), согласована 1:1 с `PIPELINE_DOCS.md` §2§3; источник
истины — код. `PIPELINE_DOCS.md` обновляется ссылкой + отметкой о реализации машинного слоя.
5. **Без изменений** `STAGE_TRANSITIONS`, состава `QG_CHECKS`, API, схемы БД.
## Альтернативы
- Общий «умный» verdict-резолвер (поле+токены для всех гейтов) — отклонён: различия token-логики
→ риск тонкого регресса на гейте при self-hosting. Унифицируем только парс YAML.
- Класс/новый пакет — отклонён: состояния нет, лишний blast radius.
- Hard-fail валидатор по умолчанию — отклонён (NFR-3: self-block собственного деплоя).
- Сторонняя `python-frontmatter` — отклонена: лишняя зависимость ради ~30 строк.
## Последствия
- **+** Конец дублирования/рассинхрона парсинга; writer+валидатор+схема готовы к ORCH-52d;
спека handoff закрывает пробел контракта стадий.
- **+** Нулевая регрессия по построению: семантика и reason-строки 1:1, валидатор инертен при
дефолте, never-raise сохранён, enduro 1:1.
- **** Унификация частичная (парс, не семантика); strict-режим «спящий» до ORCH-52d.
- **Обратимость:** `frontmatter_validation_strict=False` ⇒ прежнее поведение; перевод гейтов
поведенчески инвариантен.
- **Риск:** первый боевой `autoDeploy` орка (ORCH-089) — наблюдение за стадией `deploy`
(`docs/work-items/ORCH-076/10-tech-risks.md`).
## Связи
- Опирается: adr-0019 (pipeline-docs-standard, ORCH-075), ORCH-016 (reader), ORCH-047
(3-полевой tester), adr-0012 (security-гейт), adr-0018 (auto-label/`autoDeploy`).
- Готовит: ORCH-52d (эмиссия полной схемы агентами; возможное включение strict).

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-52b: стандарт документов (docs/_templates + манифест стадий + ADR-naming)
Work Item ID: ORCH-075
## Description
TBD

View File

@@ -0,0 +1,111 @@
# 01 — BRD: ORCH-075 — ORCH-52b: стандарт документов (docs/_templates + манифест стадий + ADR-naming)
Work Item: **ORCH-075**
Repo: **orchestrator** (self-hosting)
Стадия: analysis
Заказчик: Owner / команда агентов оркестратора
Тип: documentation-standard (слой 1 эпика ORCH-52)
> Документ фиксирует **бизнес-требования** к созданию стандарта документов пайплайна.
> ORCH-52b — слой 1 («стандарт»): только документация (манифест + канонические шаблоны +
> конвенция ADR-naming). Любая принудительная проверка/валидатор/правка кода и промптов —
> вне scope (это ORCH-52c/52d). Источник истины для манифеста — фактические
> `STAGE_TRANSITIONS` / `QG_CHECKS` и реальные эталонные доки в репозитории, а не вымысел.
---
## 1. Бизнес-контекст и проблема
### 1.1. Текущее состояние (проверено в репо)
- Каталоги `docs/_templates/` и `docs/_standards/` **не существуют**.
- Агенты (`analyst``architect` → … → `deployer`) пишут номерные доки work item
(`00-business-request.md``17-security-report.md`) «с нуля по памяти».
- Конвенция ADR-naming `06-adr/ADR-NNN-<kebab-slug>.md` фактически уже сложилась в репо,
но **нигде не зафиксирована** как стандарт.
### 1.2. Боль
- **Разнобой структуры** между задачами: набор и порядок секций одного и того же дока
плавает от work item к work item (видно при сравнении BRD/ТЗ разных задач).
- Машинные доки-вердикты (`12-review.md`, `13-test-report.md`, `14-deploy-log.md`,
`15-staging-log.md`, `17-security-report.md`) держат критичный frontmatter-ключ
(`verdict:` / `deploy_status:` / `staging_status:` / `security_status:`), читаемый
гейтом — но единого канонического скелета с этим ключом нет → риск рассинхрона.
- Нет единой карты «какая стадия / какой агент пишет какой документ и на каком гейте он
проверяется» — онбординг новых агентских ролей и аудит покрытия затруднён.
### 1.3. Почему именно стандарт (а не сразу валидатор)
Эпик ORCH-52 разбит на слои, чтобы **сначала зафиксировать договорённость (golden source
документации)**, а уже потом, отдельной задачей (52c), навешивать машинную проверку
frontmatter/шаблонов на гейте. Стандарт без кода — обратимый, низкорисковый, не трогает
работающий прод-конвейер (self-hosting). Это снижает групповой риск.
## 2. Объём (scope)
### 2.1. В объёме (ORCH-52b)
1. **Манифест** `docs/_standards/PIPELINE_DOCS.md`: таблица «стадия → документ» с
владельцем-агентом, категорией (`required` / `when-applicable` / `optional`), стадией
написания и гейтом/механизмом проверки — **сверенная с фактическими `QG_CHECKS` и
`stage_engine`**.
2. **Канонические шаблоны** `docs/_templates/*` — скелеты (frontmatter при необходимости +
обязательные секции) для всех номерных доков из реального набора.
3. **Конвенция ADR-naming** — зафиксировать сложившийся формат `06-adr/ADR-NNN-<kebab-slug>.md`
(нумерация с `001`, где живёт, как формируется slug); раздел в манифесте/стандарте.
4. Ссылки на новый стандарт в `CLAUDE.md` и `docs/architecture/README.md`; запись в
`CHANGELOG.md`.
### 2.2. Вне объёма (явно — это ORCH-52c / 52d)
- Frontmatter-валидатор в коде; writer-контракт; принудительная проверка наличия/структуры
шаблонов на Quality Gate.
- Любые изменения `QG_CHECKS` / `STAGE_TRANSITIONS` / `check_*` / `src/stage_engine.py` /
схемы БД.
- Правка системных промптов агентов (`.openclaw/agents/*`) — это слой 52d.
- Массовое приведение **уже существующих** доков прошлых задач к новому шаблону (ретро-фит).
## 3. Заинтересованные стороны
| Роль | Интерес |
|------|---------|
| Owner | Единообразие и аудитопригодность документации проекта |
| Агенты analyst/architect | Готовый скелет → меньше расхождений, быстрее старт |
| Агенты reviewer/tester/deployer | Предсказуемый frontmatter машинных доков |
| ORCH-52c (downstream) | Стандарт = база для frontmatter-валидатора/writer-контракта |
## 4. Бизнес-требования (BR)
| ID | Требование |
|----|------------|
| BR-1 | Создан `docs/_standards/PIPELINE_DOCS.md` — манифест, покрывающий **все** номерные доки реального набора (00,01,02,03,04,06,07,08,10,12,13,14,15,16,17) с владельцем-агентом и категорией. |
| BR-2 | Каждому `required` и `when-applicable` доку соответствует канонический шаблон в `docs/_templates/` (frontmatter при необходимости + обязательные секции). |
| BR-3 | Формат ADR-naming `06-adr/ADR-NNN-<kebab-slug>.md` зафиксирован в стандарте и совпадает с реальными ADR в репо. |
| BR-4 | Манифест и шаблоны **согласованы с фактическими эталонными доками** (ORCH-088/073/089/071): нет секций, которых никто не пишет, и наоборот; frontmatter-ключи машинных доков совпадают с тем, что реально парсят гейты. |
| BR-5 | Обновлены `CLAUDE.md` и `docs/architecture/README.md` со ссылкой на стандарт; добавлена запись в `CHANGELOG.md`. |
| BR-6 | Манифест отражает категорию проверки документа фактическим механизмом: какие доки несут machine-verdict frontmatter, читаемый гейтом, а какие информационные (не гейтятся). |
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| NFR-1 | **Нулевой риск для прода:** изменения — только под `docs/` (+ `CLAUDE.md`/`CHANGELOG.md`). Ни строки кода/гейтов. |
| NFR-2 | **Достоверность:** все утверждения манифеста о стадии/агенте/гейте проверяемы по `src/stages.py` / `src/qg/checks.py` / `src/stage_engine.py`. |
| NFR-3 | **Не изобретать:** шаблоны выведены из существующих эталонов, не из фантазии; новые секции не вводятся. |
| NFR-4 | **Читаемость:** манифест — на русском, в стиле существующей документации проекта; таблицы машиночитаемо-аккуратные. |
| NFR-5 | **Обратимость:** удаление новых файлов полностью откатывает изменение без следов в поведении системы. |
## 6. Допущения и ограничения
- Реальный набор номерных доков и их частота взяты из `docs/work-items/` (факты в описании
задачи); считаем его авторитетным на момент задачи.
- Эталонные («golden») задачи для извлечения скелетов — ORCH-088, ORCH-073, ORCH-089, ORCH-071.
- `09-review.md` — legacy fallback (канон — `12-review.md`); в манифест как канон **не**
вводится, при необходимости упоминается примечанием.
- Стадия `monitoring` для `16-post-deploy-log.md` — пост-`done` наблюдение (ORCH-021), не
ребро `STAGE_TRANSITIONS`; манифест это отражает явно.
## 7. Критерии успеха
- Любой агент, открыв `docs/_standards/PIPELINE_DOCS.md`, понимает: какой документ он пишет
на своей стадии, в какой категории, с каким frontmatter и где он проверяется.
- Для каждого `required`/`when-applicable` дока существует шаблон, который можно скопировать
и заполнить без догадок о структуре.
- ADR-naming больше не «устная традиция», а записанная конвенция.
- Полный набор уточняющих PASS/FAIL — в `03-acceptance-criteria.md`.
## 8. Риски
Технические риски и митигейшн ведёт архитектор в `10-tech-risks.md`. Ключевой бизнес-риск —
**рассинхрон стандарта с кодом** (манифест описал гейт, которого нет, или наоборот): митигируется
NFR-2 (сверка с `src/`) и AC-1/AC-4.

View File

@@ -0,0 +1,141 @@
# 02 — ТЗ (TRZ): ORCH-075 — ORCH-52b: стандарт документов
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные артефакты к созданию** (манифест + шаблоны + раздел ADR-naming) и
> их обязательное содержимое, выведенное из фактических `STAGE_TRANSITIONS`/`QG_CHECKS` и
> эталонных доков. Это **docs-only** изменение: исходный код, гейты, схема БД, промпты —
> НЕ затрагиваются (см. §7). Архитектурное обоснование/решения — задача архитектора (06-adr).
## 1. Сводка изменения
Создать каталоги `docs/_standards/` и `docs/_templates/`, наполнить их манифестом
«стадия→документ», каноническими скелетами номерных доков и зафиксировать ADR-naming.
Обновить точки-ссылки (`CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`).
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `docs/_standards/PIPELINE_DOCS.md` | **создать** — манифест стадия→документ + раздел ADR-naming |
| `docs/_templates/00-business-request.md` | **создать** — шаблон |
| `docs/_templates/01-brd.md` | **создать** |
| `docs/_templates/02-trz.md` | **создать** |
| `docs/_templates/03-acceptance-criteria.md` | **создать** |
| `docs/_templates/04-test-plan.yaml` | **создать** |
| `docs/_templates/06-adr-ADR-NNN-slug.md` | **создать** — шаблон ADR (имя файла шаблона без коллизии с реальной нумерацией) |
| `docs/_templates/07-infra-requirements.md` | **создать** (when-applicable) |
| `docs/_templates/08-data-requirements.md` | **создать** (when-applicable) |
| `docs/_templates/10-tech-risks.md` | **создать** |
| `docs/_templates/12-review.md` | **создать** (frontmatter `verdict:`) |
| `docs/_templates/13-test-report.md` | **создать** (frontmatter `result:`) |
| `docs/_templates/14-deploy-log.md` | **создать** (frontmatter `deploy_status:`) |
| `docs/_templates/15-staging-log.md` | **создать** (frontmatter `staging_status:`) |
| `docs/_templates/16-post-deploy-log.md` | **создать** (frontmatter `post_deploy_status:`) |
| `docs/_templates/17-security-report.md` | **создать** (frontmatter `security_status:`) |
| `CLAUDE.md` | **изменить** — ссылка на стандарт в разделе «Артефакты задачи» / «Правила для агентов» |
| `docs/architecture/README.md` | **изменить** — ссылка на стандарт |
| `CHANGELOG.md` | **изменить** — запись в `## [Unreleased]` |
> Точное имя файла-шаблона ADR оставлено на усмотрение разработчика/архитектора при условии,
> что **внутри** шаблона и в манифесте зафиксирован реальный целевой формат
> `06-adr/ADR-NNN-<kebab-slug>.md` (см. §3, FR-3).
## 3. Функциональные требования
### FR-1 — Манифест `docs/_standards/PIPELINE_DOCS.md`
Содержит таблицу-манифест, покрывающую **все** номерные доки реального набора. Для каждого —
колонки: `Документ`, `Владелец-агент`, `Категория`, `Стадия написания`, `Гейт/механизм
проверки`, `Frontmatter machine-key (если есть)`. Манифест ДОЛЖЕН соответствовать
ground-truth ниже (сверено по `src/`):
| Документ | Владелец-агент | Категория | Стадия написания | Гейт / проверка | Machine-key |
|----------|----------------|-----------|------------------|-----------------|-------------|
| `00-business-request.md` | система (Plane webhook `_create_initial_docs`) / заказчик | required | `created` (инициализация) | не гейтится (вход) | — |
| `01-brd.md` | analyst | required | `analysis` | exit-гейт `analysis→architecture` = `check_analysis_approved` (Approved + полнота файлов); helper `check_analysis_complete` (наличие) | — |
| `02-trz.md` | analyst | required | `analysis` | то же | — |
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
| `06-adr/ADR-NNN-<slug>.md` | architect | required | `architecture` | `check_architecture_done` (наличие каталога/ADR) | — |
| `07-infra-requirements.md` | architect | when-applicable | `architecture` | `check_architecture_done` (учитывается при наличии) | — |
| `08-data-requirements.md` | architect | when-applicable | `architecture` | информационный (гейтом не парсится) | — |
| `10-tech-risks.md` | architect | required | `architecture` | информационный (гейтом не парсится) | — |
| `12-review.md` | reviewer | required | `review` | `check_reviewer_verdict` | `verdict:` (APPROVED\|REQUEST_CHANGES) |
| `13-test-report.md` | tester | required | `testing` | `check_tests_passed` | `result:`/`verdict:`/`status:` (PASS\|FAIL\|BLOCKED) |
| `14-deploy-log.md` | deployer / deploy-finalizer | required | `deploy` | `check_deploy_status` | `deploy_status:` (SUCCESS\|FAILED) |
| `15-staging-log.md` | deployer | required (self-hosting) | `deploy-staging` | `check_staging_status` (self-hosting; иначе N/A) | `staging_status:` (SUCCESS\|FAILED) |
| `16-post-deploy-log.md` | post-deploy-monitor | when-applicable | пост-`done` наблюдение (ORCH-021, не ребро `STAGE_TRANSITIONS`) | информационный (гейтом не парсится) | `post_deploy_status:` |
| `17-security-report.md` | security-гейт (детерминированный, ORCH-022) | when-applicable | под-гейт ребра `deploy-staging→deploy` | `check_security_gate` | `security_status:` (PASS\|FAIL) |
Примечания манифеста (обязательны):
- Под-гейты ребра `deploy-staging→deploy` (`check_security_gate``check_branch_mergeable`
`check_staging_image_fresh`) — **не** строки `STAGE_TRANSITIONS`, а врезки в `advance_stage`.
- `09-review.md` — legacy fallback; канон — `12-review.md` (упомянуть примечанием, в основную
таблицу как канон не вносить).
- Категория `when-applicable` = пишется при наличии соответствующего предмета (инфра/данные/
security/post-deploy); её отсутствие — не нарушение.
### FR-2 — Шаблоны `docs/_templates/*`
Каждый шаблон — копируемый скелет. Обязательные элементы по типам (выведено из эталонов
ORCH-088/073/089/071):
- **Документы БЕЗ frontmatter** (`00`,`01`,`02`,`03`,`06-adr`,`07`,`08`,`10`): заголовок `#`,
строка метаданных `Work Item / Repo / Стадия`, и фиксированные `##`-секции (ниже §FR-2.1).
- **YAML-only**: `04-test-plan.yaml` — корневые ключи + список `tests:` (ниже §FR-2.2).
- **Документы С YAML-frontmatter** (`12`,`13`,`14`,`15`,`16`,`17`): блок `---…---` с
machine-key из таблицы FR-1 + body-секции.
#### FR-2.1 Обязательные секции по документу (минимальный канон)
- `00-business-request.md`: `# Business Request: <subject>`; строка `Work Item ID:`; `## Description`.
- `01-brd.md`: `## 1. Бизнес-контекст и проблема`, `## 2. Объём (scope)` (с `### В объёме`/`### Вне объёма`), `## 3. Заинтересованные стороны`, `## 4. Бизнес-требования (BR)`, `## 5. Нефункциональные требования (NFR)`, `## 6. Допущения и ограничения`, `## 7. Критерии успеха`, `## 8. Риски`.
- `02-trz.md`: `## 1. Сводка изменения`, `## 2. Задействованные модули`, `## 3. Функциональные требования`, `## 4. Изменения API`, `## 5. Изменения схемы БД`, `## 6. Требования к QG checks`, `## 7. Совместимость / регресс`.
- `03-acceptance-criteria.md`: преамбула формата; повторяемый блок `## AC-N — <title>` с `**Условие:**`, `- **PASS:**`, `- **FAIL:**`; опц. `## Сводная матрица AC ↔ FR/BR`.
- `06-adr (шаблон)`: `# ADR-NNN: <title>`, метаданные (`Work Item`,`Стадия: architecture`,`Сквозная регистрация:`), `## Статус`, `## Контекст`, `## Решение` (с `### Сводка` и `### D1 — …`), `## Альтернативы`, `## Последствия`, `## Ссылки`.
- `07-infra-requirements.md`: `# 07 — Инфра-требования`, нумерованные `## I-N. <topic>`.
- `08-data-requirements.md`: `# 08 — Требования к данным`, секции по таблицам/колонкам/миграциям.
- `10-tech-risks.md`: `# 10 — Технические риски`, таблица `| ID | Риск | Вер. | Влия. | Митигейшн |`, `## Сводный вывод`.
- `12-review.md`: frontmatter `type: review` / `work_item_id:` / `verdict:` / `version:`; body `## Summary`, `## Оси проверки`, `## Findings` (`### P0`/`### P1`/`### P2`), `## Документация`.
- `13-test-report.md`: frontmatter `type: test-report` / `work_item_id:` / `result:`; body `## Окружение`, `## Результаты` (`### Полный регресс`, `### Профильные сюиты`, `### Сопоставление с тест-планом`, `### Сопоставление с критериями приёмки`).
- `14-deploy-log.md`: frontmatter `deploy_status:` / `work_item:` / `hook_exit_code:` / `deployed_by:`; body — краткое описание деплоя.
- `15-staging-log.md`: frontmatter `staging_status:` / `timestamp:` / `base_url:`; body — `# Staging Gate Log` + результаты проверок.
- `16-post-deploy-log.md`: frontmatter `post_deploy_status:` / `action_taken:` / `work_item:`; body — окно наблюдения/серия/решение.
- `17-security-report.md`: frontmatter `security_status:` / `work_item:`; body — secret-scan + dependency-audit результаты.
#### FR-2.2 `04-test-plan.yaml`
Корень: `work_item:`, `title:`, `framework: pytest`, опц. `scope:`/`notes:`. Список `tests:`
с элементами `{ id: TC-NN, type: unit|integration, description, module: tests/…, expected: PASS }`.
### FR-3 — Конвенция ADR-naming
Зафиксировать в `PIPELINE_DOCS.md` отдельным разделом:
- Путь: `docs/work-items/<plane-id>/06-adr/`.
- Имя: `ADR-NNN-<kebab-slug>.md`, `NNN` с `001`, инкремент при нескольких ADR в одной задаче.
- `slug` — kebab-case (нижний регистр, дефисы), отражает суть решения.
- Сквозные (cross-cutting) решения дублируются в `docs/architecture/adr/adr-NNNN-<slug>.md`
(4-значная глобальная нумерация) — это уже существующая конвенция, лишь зафиксировать.
- Примеры из репо: `ADR-001-serial-gate`, `ADR-001-auto-label-gates`, `ADR-001-merge-verify-gate`.
### FR-4 — Точки-ссылки
- `CLAUDE.md`: в разделе «Артефакты задачи» и/или «Правила для агентов» добавить ссылку на
`docs/_standards/PIPELINE_DOCS.md` и `docs/_templates/` как golden source структуры доков.
- `docs/architecture/README.md`: добавить ссылку/абзац о стандарте документов.
- `CHANGELOG.md`: запись в `## [Unreleased]` (`docs:` тип).
## 4. Изменения API
Нет. Эндпоинты не добавляются и не меняются.
## 5. Изменения схемы БД
Нет. Таблицы/миграции не затрагиваются.
## 6. Требования к новым/изменённым QG checks
Нет. `QG_CHECKS` и `check_*` **не трогаются** (это ORCH-52c). Манифест лишь **документирует**
текущее поведение гейтов.
## 7. Совместимость / регресс
- Изменения только под `docs/` + `CLAUDE.md` + `CHANGELOG.md`. Поведение рантайма неизменно.
- Существующие доки прошлых задач не модифицируются (нет ретро-фита).
- `09-review.md` (legacy) сохраняется как fallback; манифест канонизирует `12-review.md`.
- Удаление новых файлов → полный откат без следов (NFR-5).
## 8. Артефакты, создаваемые/обновляемые по pipeline
Создаются: `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*` (15 шаблонов).
Обновляются: `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`.
Downstream-доки самой задачи ORCH-075 (`06-adr`, `10-tech-risks`, `12-review`, `13-test-report`,
`14-deploy-log`, `15-staging-log`) — по штатному конвейеру.

View File

@@ -0,0 +1,104 @@
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-075 — ORCH-52b: стандарт документов
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Скоп — только создание стандарта/шаблонов/манифеста (docs-only).
> Критерии унаследованы из AC задачи и расширены проверяемыми условиями. Любой машинный/ручной
> reviewer проверяет их буквально по файлам репозитория.
---
## AC-1 — Манифест создан и покрывает весь реальный набор
**Условие:** существует `docs/_standards/PIPELINE_DOCS.md` с таблицей-манифестом.
- **PASS:** файл существует; манифест содержит строки для **всех** номерных доков реального
набора — `00, 01, 02, 03, 04, 06, 07, 08, 10, 12, 13, 14, 15, 16, 17`; для каждого указаны
владелец-агент (analyst/architect/developer/reviewer/tester/deployer/система) и категория
(`required` / `when-applicable` / `optional`).
- **FAIL:** файла нет; пропущен хотя бы один документ набора; у дока отсутствует владелец или
категория.
---
## AC-2 — Шаблоны созданы для каждого required/when-applicable дока
**Условие:** существует `docs/_templates/` с каноническими скелетами.
- **PASS:** для каждого `required` и `when-applicable` дока есть файл-шаблон; в шаблоне
присутствуют (а) frontmatter с machine-key там, где он требуется по FR-1 (`12``verdict:`,
`13``result:`, `14``deploy_status:`, `15``staging_status:`, `16``post_deploy_status:`,
`17``security_status:`), и (б) обязательные `##`-секции из ТЗ §FR-2.1.
- **FAIL:** отсутствует шаблон для какого-либо required/when-applicable дока; в шаблоне
машинного дока нет требуемого frontmatter-ключа; набор секций произвольный, не из ТЗ.
---
## AC-3 — ADR-naming зафиксирован
**Условие:** в стандарте есть раздел про ADR-naming.
- **PASS:** зафиксирован формат `06-adr/ADR-NNN-<kebab-slug>.md` (NNN с `001`), путь
(`docs/work-items/<plane-id>/06-adr/`), правило формирования slug (kebab-case) и связь со
сквозным реестром `docs/architecture/adr/adr-NNNN-<slug>.md`; приведён ≥1 реальный пример.
- **FAIL:** ADR-naming не описан, либо описанный формат не совпадает с реальными ADR в репо
(напр. указана нумерация не с `001`, неверный путь, неверный регистр slug).
---
## AC-4 — Согласованность с фактическими эталонами
**Условие:** манифест и шаблоны соответствуют реальным эталонным докам (ORCH-088/073/089/071)
и фактическому коду.
- **PASS:** в шаблонах нет секций, которых никто не пишет в эталонах; все секции эталонов,
входящие в общий канон, присутствуют; frontmatter-ключи машинных доков совпадают с тем, что
реально парсят `src/qg/checks.py` (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/
`security_status:`); привязка «документ→стадия→гейт» совпадает с `src/stages.py`
(`STAGE_TRANSITIONS`).
- **FAIL:** шаблон вводит выдуманную секцию; манифест приписывает доку неверную стадию/гейт/
агента; frontmatter-ключ в шаблоне не тот, что читает гейт.
---
## AC-5 — Ссылки и CHANGELOG обновлены
**Условие:** точки-ссылки и журнал изменений отражают новый стандарт.
- **PASS:** `CLAUDE.md` и `docs/architecture/README.md` содержат ссылку на
`docs/_standards/PIPELINE_DOCS.md` (и/или `docs/_templates/`); в `CHANGELOG.md` добавлена
запись в `## [Unreleased]` типа `docs:`.
- **FAIL:** хотя бы одна из трёх точек не обновлена.
---
## AC-6 — Код гейтов НЕ изменён
**Условие:** изменение строго docs-only.
- **PASS:** `git diff` не содержит изменений в `src/qg/checks.py` (`QG_CHECKS`/`check_*`),
`src/stages.py` (`STAGE_TRANSITIONS`), `src/stage_engine.py`, схеме БД и любом коде гейтов;
затронуты только `docs/**`, `CLAUDE.md`, `CHANGELOG.md` (+ опционально новые файлы тестов).
- **FAIL:** изменён любой из перечисленных кодовых модулей/гейтов/схемы.
---
## AC-7 — Манифест различает machine-verdict и информационные доки
**Условие:** манифест честно отражает механизм проверки.
- **PASS:** документы, чей frontmatter читает гейт (`12,13,14,15,17`), помечены своим
machine-key и гейтом; информационные (`00,08,10,16`) явно помечены как не гейтящиеся;
под-гейты ребра `deploy-staging→deploy` (security/merge/image-freshness) отмечены как врезки
в `advance_stage`, а не строки `STAGE_TRANSITIONS`.
- **FAIL:** информационный док представлен как гейтящийся (или наоборот); под-гейты выданы за
стадии.
---
## Сводная матрица AC ↔ BR
| AC | Покрывает BR |
|----|--------------|
| AC-1 | BR-1 |
| AC-2 | BR-2 |
| AC-3 | BR-3 |
| AC-4 | BR-4, BR-6 |
| AC-5 | BR-5 |
| AC-6 | NFR-1, NFR-5 |
| AC-7 | BR-6, NFR-2 |

View File

@@ -0,0 +1,136 @@
work_item: ORCH-075
title: "ORCH-52b — стандарт документов (docs/_standards + docs/_templates + ADR-naming)"
scope: "docs-only: проверяется НАЛИЧИЕ и СТРУКТУРА новых файлов-стандартов/шаблонов; код гейтов не трогается"
framework: pytest
notes: >
Изменение документационное. Тесты — лёгкие структурные проверки (existence + наличие
обязательных секций/frontmatter-ключей), новый файл tests/test_orch_52b_docs_standard.py.
Тесты НЕ меняют QG_CHECKS/STAGE_TRANSITIONS и не вводят новый гейт (это ORCH-52c). Полный
регресс tests/ должен остаться зелёным (отсутствие регресса от docs-изменения).
tests:
- id: TC-01
type: integration
description: "docs/_standards/PIPELINE_DOCS.md существует и непустой"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-02
type: integration
description: "Манифест PIPELINE_DOCS.md упоминает все номерные доки набора: 00,01,02,03,04,06,07,08,10,12,13,14,15,16,17"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-03
type: integration
description: "Манифест указывает владельца-агента для каждого дока (analyst/architect/reviewer/tester/deployer/система упомянуты)"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-04
type: integration
description: "Манифест содержит категории required / when-applicable / optional"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-05
type: integration
description: "Каталог docs/_templates/ существует и содержит шаблоны для всех required/when-applicable доков"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-06
type: integration
description: "Шаблон 12-review содержит frontmatter-ключ verdict:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-07
type: integration
description: "Шаблон 13-test-report содержит frontmatter-ключ result:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-08
type: integration
description: "Шаблон 14-deploy-log содержит frontmatter-ключ deploy_status:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-09
type: integration
description: "Шаблон 15-staging-log содержит frontmatter-ключ staging_status:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-10
type: integration
description: "Шаблон 17-security-report содержит frontmatter-ключ security_status:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-11
type: integration
description: "Шаблон 16-post-deploy-log содержит frontmatter-ключ post_deploy_status:"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-12
type: integration
description: "Шаблон 01-brd содержит обязательные секции: Бизнес-контекст, Объём, Бизнес-требования, NFR"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-13
type: integration
description: "Шаблон 02-trz содержит обязательные секции: Задействованные модули, Изменения API, Изменения схемы БД, QG checks"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-14
type: integration
description: "Шаблон 03-acceptance-criteria содержит блок AC-N с метками PASS и FAIL"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-15
type: integration
description: "Шаблон 04-test-plan.yaml — валидный YAML с ключами work_item и tests (список с id/type/description/module/expected)"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-16
type: integration
description: "Раздел ADR-naming присутствует и фиксирует формат ADR-NNN-<slug>.md с нумерацией с 001 и kebab-slug"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-17
type: integration
description: "ADR-naming в стандарте совпадает с реальными ADR в репо (напр. существует docs/work-items/ORCH-088/06-adr/ADR-001-*.md)"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-18
type: integration
description: "CLAUDE.md содержит ссылку на docs/_standards/PIPELINE_DOCS.md"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-19
type: integration
description: "docs/architecture/README.md содержит ссылку на стандарт документов"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-20
type: integration
description: "CHANGELOG.md содержит запись об ORCH-52b/ORCH-075 в разделе Unreleased"
module: tests/test_orch_52b_docs_standard.py
expected: PASS
- id: TC-21
type: integration
description: "Регресс: полный прогон pytest tests/ зелёный (docs-изменение не ломает существующие тесты)"
module: tests/
expected: PASS

View File

@@ -0,0 +1,160 @@
# ADR-001: Стандарт документов пайплайна (docs/_standards + docs/_templates + ADR-naming)
Work Item: **ORCH-075** (ORCH-52b — слой 1 эпика ORCH-52)
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0019-pipeline-docs-standard.md`** (решение
кросс-каттинговое — задаёт правила доко-письма для ВСЕХ агентских ролей).
## Статус
Proposed
## Контекст
Агенты конвейера (`analyst → architect → developer → reviewer → tester → deployer` + системные
акторы deploy-finalizer / post-deploy-monitor / security-гейт) пишут номерные документы work item
(`00-business-request.md``17-security-report.md`) «с нуля по памяти». Каталогов
`docs/_standards/` и `docs/_templates/` не существует (проверено в репо). Следствия:
- **Разнобой структуры** одного и того же документа от задачи к задаче (набор/порядок секций
плавает) — затрудняет ревью и онбординг новых ролей.
- **Риск рассинхрона машинных вердиктов.** Доки `12-review.md` / `13-test-report.md` /
`14-deploy-log.md` / `15-staging-log.md` / `17-security-report.md` несут frontmatter-ключ,
который читает гейт (`verdict:` / `result:` / `deploy_status:` / `staging_status:` /
`security_status:`), но единого канонического скелета с этим ключом нет → агент может выдать
ключ не того имени/регистра и уронить гейт ложно.
- **Нет карты «стадия → агент → документ → гейт».** Какая роль пишет какой документ и где он
проверяется — нигде не зафиксировано целостно.
Эпик ORCH-52 намеренно разбит на слои: **сначала зафиксировать договорённость** (golden source
структуры доков), и лишь потом, отдельной задачей (52c), навесить машинную проверку
frontmatter/шаблонов на гейте. Слой 1 (эта задача) — **только документация**: манифест,
канонические шаблоны, конвенция ADR-naming. Любой валидатор/правка кода/правка промптов — вне
scope. Ключевое архитектурное ограничение задачи (NFR-1): **ни строки кода/гейтов** — изменения
строго под `docs/**` (+ `CLAUDE.md` / `CHANGELOG.md`).
Ground-truth для манифеста — **фактические** `STAGE_TRANSITIONS` (`src/stages.py`), `QG_CHECKS` /
`check_*` (`src/qg/checks.py`), `src/stage_engine.py` и реальные эталонные доки (ORCH-088/073/089/071),
а не вымысел. Сверка проведена на стадии architecture (см. §Решение D5).
## Решение
### Сводка
Создать **документационный стандарт** из трёх артефактов, выведенный из фактического кода и
эталонных доков, и подключить его ссылками из точек-онбординга. Никаких рантайм-изменений.
- `docs/_standards/PIPELINE_DOCS.md` — манифест-карта «стадия → документ → агент → категория →
гейт/механизм → frontmatter machine-key» + раздел ADR-naming.
- `docs/_templates/*` — копируемые скелеты для каждого `required` / `when-applicable` дока.
- Ссылки из `CLAUDE.md` и `docs/architecture/README.md`; запись в `CHANGELOG.md`.
### D1 — Местоположение и разделение «стандарт» vs «шаблон»
Два каталога с раздельной ответственностью: `docs/_standards/`**описательный** golden source
(манифест, конвенции, человекочитаемая карта); `docs/_templates/`**копируемые** скелеты для
заполнения. Префикс `_` выводит служебные каталоги наверх листинга и визуально отделяет их от
`docs/work-items/` / `docs/architecture/` / `docs/operations/`. Шаблоны — НЕ work item, не имеют
`<plane-id>`, не парсятся гейтами (живут вне `docs/work-items/`), поэтому не влияют на
`check_architecture_done` / `check_analysis_complete` и не попадают под ретро-фит.
### D2 — Манифест как производная от кода, а не параллельный источник истины
`PIPELINE_DOCS.md` **документирует** текущее поведение гейтов, но НЕ становится их источником
истины (источник остаётся `src/`). Это устраняет класс «манифест разошёлся с кодом»: при будущем
изменении гейта (ORCH-52c+) правка кода первична, манифест — следом. Манифест честно различает:
- **machine-verdict доки** (`12,13,14,15,17`) — несут frontmatter-ключ, читаемый гейтом; в
манифесте помечены ключом и именем `check_*`;
- **информационные доки** (`00,08,10,16`) — гейтом не парсятся; помечены явно как не-гейтящиеся
(чтобы не возникало ложного ожидания, что их структура что-то блокирует).
Под-гейты ребра `deploy-staging → deploy` (`check_security_gate``check_branch_mergeable`
`check_staging_image_fresh`) в манифесте отмечаются как **врезки в `advance_stage`**, а НЕ строки
`STAGE_TRANSITIONS` — иначе карта соврёт о топологии машины стадий (AC-7).
### D3 — Шаблоны выведены из эталонов, новые секции не изобретаются
Скелеты извлекаются из реальных «golden» задач (ORCH-088/073/089/071) и текущей задачи ORCH-075.
Инвариант (NFR-3): **в шаблоне нет секции, которой никто не пишет в эталонах**, и наоборот — общий
канон секций эталона присутствует. Минимальный обязательный набор секций по каждому документу
зафиксирован в TRZ §FR-2.1 и является контрактом приёмки (AC-2/AC-4). Документы с машинным
вердиктом несут в шаблоне точный frontmatter-ключ из ground-truth таблицы (D5), чтобы скопированный
скелет проходил гейт без догадок.
### D4 — ADR-naming: канонизация сложившейся традиции, не новый формат
Зафиксировать **уже существующий** формат, не вводя нового:
- Путь: `docs/work-items/<plane-id>/06-adr/`.
- Имя: `ADR-NNN-<kebab-slug>.md`; `NNN` с `001`, инкремент при нескольких ADR в одной задаче.
- `slug` — kebab-case (нижний регистр, дефисы), отражает суть решения.
- Кросс-каттинговые решения **дублируются** в глобальном реестре
`docs/architecture/adr/adr-NNNN-<slug>.md` (4-значная сквозная нумерация) — это уже действующая
конвенция (подтверждено: реестр идёт до `adr-0018`), лишь записывается.
- Примеры из репо (проверены): `ADR-001-serial-gate`, `ADR-001-auto-label-gates`,
`ADR-001-merge-verify-gate`.
Сам этот ADR следует конвенции и дублируется как `adr-0019` — стандарт демонстрирует себя.
### D5 — Достоверность: сверка манифеста с `src/` на стадии architecture (NFR-2)
Перед фиксацией манифеста ground-truth сверен с кодом. Подтверждено:
| Документ | Гейт / механизм | Machine-key | Подтверждено в |
|----------|-----------------|-------------|----------------|
| `0104` | `check_analysis_approved` (exit `analysis→architecture`); helper `check_analysis_complete` (наличие `01/02/03/04`) | — | `stages.py`, `qg/checks.py:check_analysis_complete` |
| `06-adr/` | `check_architecture_done` (наличие каталога `06-adr/` ≥1 файл ИЛИ `07-infra-requirements.md`) | — | `qg/checks.py:check_architecture_done` |
| `12-review.md` | `check_reviewer_verdict` | `verdict:` | `qg/checks.py` |
| `13-test-report.md` | `check_tests_passed` | `result:`/`verdict:`/`status:` (три равноранговых, ORCH-047) | `qg/checks.py:_parse_tests_verdict` |
| `14-deploy-log.md` | `check_deploy_status` | `deploy_status:` | `qg/checks.py:_parse_deploy_status` |
| `15-staging-log.md` | `check_staging_status` (self-hosting; иначе N/A — ORCH-35) | `staging_status:` | `qg/checks.py:_parse_staging_status` |
| `17-security-report.md` | `check_security_gate` (под-гейт ребра `deploy-staging→deploy`) | `security_status:` | `qg/checks.py` |
| `16-post-deploy-log.md` | информационный (пост-`done` наблюдение ORCH-021, не ребро) | `post_deploy_status:` (не гейтится) | `stage_engine.run_post_deploy_monitor` |
| `00/08/10` | не гейтятся (вход / информационные) | — | — |
`STAGE_TRANSITIONS` (проверено): `analysis→architecture→development→review→testing→deploy-staging
→deploy→done`; рёбра несут ровно `check_analysis_approved / check_architecture_done / check_ci_green
/ check_reviewer_verdict / check_tests_passed / check_staging_status / check_deploy_status`.
Под-гейты `security/merge/image-freshness` в `STAGE_TRANSITIONS` **отсутствуют** (врезки в
`advance_stage`) — подтверждает D2.
### D6 — Разграничение ответственности стадий (что пишет архитектор vs разработчик)
Эта стадия (architecture) производит **только** ADR + tech-risks (+ N/A infra/data). Сами артефакты
стандарта (`PIPELINE_DOCS.md`, `docs/_templates/*`) и правки точек-ссылок
(`CLAUDE.md` / `docs/architecture/README.md` / `CHANGELOG.md`) создаёт стадия development по TRZ §2 —
чтобы не было двойного авторства и конфликтов. Архитектор фиксирует **контракт** (что и где должно
появиться, по каким инвариантам), разработчик его **реализует**.
## Альтернативы
- **Один файл-стандарт без каталога шаблонов** — отвергнуто: шаблон должен быть копируемым
отдельным файлом (UX «скопировал и заполнил», AC-2), а не вырезкой из прозы манифеста.
- **Сразу валидатор frontmatter на гейте** — отвергнуто намеренно (это ORCH-52c): нарушило бы
NFR-1 (правка кода/гейтов) и подняло бы групповой self-hosting риск без предварительной фиксации
договорённости. Слой «стандарт» обязан предшествовать слою «проверка».
- **Манифест как источник истины для гейтов** — отвергнуто: породило бы дубль-истину и класс
«манифест ≠ код». Источник остаётся `src/`; манифест — производная (D2).
- **Положить шаблоны в `docs/work-items/_template/`** — отвергнуто: попадание под `docs/work-items/`
с `<plane-id>`-семантикой риск-фактор для гейтов наличия файлов и сканеров; служебный каталог
должен быть вне дерева work item (D1).
- **Ретро-фит существующих доков под новый шаблон** — отвергнуто (вне scope, BRD §2.2): массовая
правка истории — отдельный риск и шум; стандарт применяется к новым задачам вперёд.
- **Не заводить глобальный `adr-0019`** — отвергнуто: решение кросс-каттинговое (правила
доко-письма для всех ролей), а FR-3 сам канонизирует дублирование сквозных решений в глобальный
реестр — стандарт обязан следовать собственному правилу.
## Последствия
- **+** Единая карта «стадия → агент → документ → гейт → machine-key»; копируемые скелеты →
меньше разнобоя и ложных падений гейтов из-за неверного frontmatter-ключа; ADR-naming перестаёт
быть устной традицией; готовая база для ORCH-52c (валидатор).
- **+ Нулевой рантайм-риск (NFR-1/NFR-5):** изменения только под `docs/**` + `CLAUDE.md` +
`CHANGELOG.md`. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / `src/stage_engine.py` / схема БД —
**не трогаются**. Удаление новых файлов полностью откатывает изменение без следов в поведении
системы (обратимость).
- ** Дрейф во времени:** манифест — снимок поведения гейтов; при будущей правке гейта его нужно
обновлять вручную (до ORCH-52c, где появится проверка). Митигейшн: D2 (источник истины — код) +
reviewer-правило «обновлена ли документация» + явная привязка манифеста к именам `check_*`.
- **** Стандарт описательный, не принуждающий: агент может его проигнорировать (форсинг — 52c).
Осознанно принято как цена слоистого подхода.
- **Откат:** удалить `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*`, снять ссылки и запись
CHANGELOG — система ведёт себя в точности как до ORCH-075.
## Ссылки
- BRD: `docs/work-items/ORCH-075/01-brd.md`
- TRZ: `docs/work-items/ORCH-075/02-trz.md` (ground-truth таблица FR-1, секции FR-2.1)
- Acceptance: `docs/work-items/ORCH-075/03-acceptance-criteria.md`
- Tech-risks: `docs/work-items/ORCH-075/10-tech-risks.md`
- Глобальный реестр: `docs/architecture/adr/adr-0019-pipeline-docs-standard.md`
- Эталоны скелетов: ORCH-088 / ORCH-073 / ORCH-089 / ORCH-071 (`docs/work-items/*/`)
- Сверено по коду: `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`,
`_parse_*`), `src/stage_engine.py`.

View File

@@ -0,0 +1,25 @@
# 07 — Инфра-требования: ORCH-075 (ORCH-52b — стандарт документов)
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
## I-1. Топология / окружения
**N/A.** Изменение docs-only: создаются `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*` и
правятся `CLAUDE.md` / `docs/architecture/README.md` / `CHANGELOG.md`. Контейнеры (`orchestrator`
8500, `orchestrator-staging` 8501), Docker Compose, сеть, тома, хост mva154 — **не затрагиваются**.
## I-2. Переменные окружения / секреты
**N/A.** Новые env-переменные не вводятся; `.env` / `.env.staging` / `.env.example` не меняются;
секретов не добавляется.
## I-3. Деплой / рестарт
**N/A.** Рантайм-поведение не меняется → прод-рестарт не требуется. Изменение проходит штатный
self-hosting путь (`deploy-staging` 8501 → `deploy` 8500) как обычный PR, но эффект деплоя — лишь
появление новых docs-файлов в образе; функциональной нагрузки на рестарт нет. Self-hosting инвариант
соблюдён: **не ронять / не рестартить прод вне staging-гейта** — здесь это и не нужно.
## I-4. CI/CD
Без изменений в `.gitea/workflows/`. Добавляется один тестовый файл
`tests/test_orch_52b_docs_standard.py` (структурные проверки), исполняемый существующим pytest-шагом.
> Вывод: инфраструктурных требований нет. Файл создан для аудитопригодности (явное N/A), а не из-за
> изменения топологии.

View File

@@ -0,0 +1,28 @@
# 08 — Требования к данным: ORCH-075 (ORCH-52b — стандарт документов)
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
## Изменения схемы БД
**N/A.** Изменение docs-only. Таблицы SQLite (`jobs`, `tasks`, `job_deps`, `repo_freeze`,
`agent_runs`, `tracker_messages`, …), индексы, миграции (`init_db`) — **не затрагиваются**.
## Новые/изменённые сущности
**Нет.** Манифест и шаблоны — статические Markdown/YAML-файлы под `docs/`, вне модели данных
рантайма. Гейты наличия файлов (`check_analysis_complete` / `check_architecture_done`) сканируют
только `docs/work-items/<plane-id>/` и служебные каталоги `docs/_standards/` / `docs/_templates/` не
видят (см. ADR-001 §D1, риск TR-6).
## Frontmatter machine-keys (документируются, не вводятся)
Стандарт лишь **фиксирует** уже существующие машиночитаемые ключи, которые парсят гейты — это НЕ
новые поля данных и не изменение хранения:
| Документ | Ключ | Парсер (`src/qg/checks.py`) |
|----------|------|-----------------------------|
| `12-review.md` | `verdict:` | `check_reviewer_verdict` |
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` |
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` |
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` |
| `17-security-report.md` | `security_status:` | `check_security_gate` |
| `16-post-deploy-log.md` | `post_deploy_status:` | информационный (не гейтится) |
> Вывод: требований к данным/схеме нет. Файл создан для аудитопригодности (явное N/A).

View File

@@ -0,0 +1,28 @@
# 10 — Технические риски: ORCH-075 (ORCH-52b — стандарт документов)
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
> Изменение docs-only (NFR-1): только `docs/**` + `CLAUDE.md` + `CHANGELOG.md`. Рантайм-рисков
> деградации прода нет по построению. Основные риски — **достоверность** манифеста и **дрейф**
> стандарта относительно кода.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Рассинхрон манифеста с кодом** — манифест приписывает доку неверную стадию/гейт/агента или неверный frontmatter-ключ (напр. `12-review` → не `verdict:`). | Сред. | Сред. (вводит агентов в заблуждение, ложные ожидания) | NFR-2: ground-truth сверен с `src/stages.py` / `src/qg/checks.py` / `src/stage_engine.py` на стадии architecture (ADR-001 §D5, таблица сверки). AC-4 проверяет привязку буквально по файлам. Источник истины остаётся код (D2). |
| TR-2 | **Дрейф во времени** — будущая правка гейта (ORCH-52c+) не отражается в манифесте, т.к. форсинга нет. | Сред. | Низ. (до 52c — описательный документ) | D2 (источник истины — код, манифест производный); reviewer-правило «обновлена ли документация»; явная привязка строк манифеста к именам `check_*`; ORCH-52c добавит машинную проверку. |
| TR-3 | **Шаблон вводит выдуманную секцию** или, наоборот, упускает секцию общего канона эталонов. | Сред. | Низ. | NFR-3: скелеты выведены строго из эталонов ORCH-088/073/089/071 + ORCH-075; TRZ §FR-2.1 фиксирует минимальный обязательный набор секций; AC-2/AC-4 проверяют. |
| TR-4 | **Неверный machine-key в шаблоне машинного дока** — скопированный скелет уронит гейт ложно (напр. `deploy_status` написан `Deploy-Status`/иной регистр). | Низ. | Выс. (если бы дошло до прода — ложный откат БАГ-8) | Ключи в шаблонах берутся ДОСЛОВНО из `_parse_*` (`deploy_status`/`staging_status`/`security_status`/`verdict`/`result`); парсеры делают `.upper()` на значении, но имя ключа чувствительно — шаблон фиксирует точное имя. AC-2 проверяет наличие ключа. Гейты при этом **не трогаются** (docs-only). |
| TR-5 | **Коллизия имени файла-шаблона ADR** с реальной нумерацией (`06-adr/ADR-NNN-…`). | Низ. | Низ. | TRZ §2: имя шаблона ADR без `<plane-id>`-контекста и вне `docs/work-items/` (напр. `docs/_templates/06-adr-ADR-NNN-slug.md`); внутри фиксируется реальный целевой путь/формат. |
| TR-6 | **Шаблоны парсятся гейтами наличия файлов** (`check_architecture_done` / `check_analysis_complete` ловят `docs/_templates/*`). | Оч.низ. | Сред. | D1: служебные каталоги `docs/_standards/` / `docs/_templates/` лежат ВНЕ `docs/work-items/<plane-id>/`; гейты сканируют только путь work item → шаблоны структурно невидимы гейтам. |
| TR-7 | **Регресс существующих тестов** от docs-изменения. | Оч.низ. | Сред. | Изменения не трогают `src/`; новый тест `tests/test_orch_52b_docs_standard.py` — только структурные проверки наличия/секций; TC-21 требует зелёного полного `pytest tests/`. |
| TR-8 | **CHANGELOG-конфликт при merge** (`## [Unreleased]` правят параллельные задачи). | Низ. | Низ. | Корневой `.gitattributes` `CHANGELOG.md merge=union` (ORCH-073 FR-4) авто-сливает append-правки без конфликта. |
## Сводный вывод
Риск для прод-конвейера (self-hosting) — **отсутствует по построению**: изменение docs-only,
полностью обратимо (NFR-5), `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД не затрагиваются
(AC-6). Доминирующий класс рисков — **достоверность и дрейф** манифеста (TR-1/TR-2); закрыт сверкой
с `src/` на стадии architecture (ADR-001 §D5) и принципом «источник истины — код, манифест —
производная». Эскалация `arch:major-change` не требуется (нет новой стадии/QG/компонента/смены БД).
Возврат в анализ не требуется — ТЗ удовлетворяется без нарушения принципов архитектуры.

View File

@@ -0,0 +1,71 @@
---
type: review
work_item_id: ORCH-075
verdict: APPROVED
version: 1
---
# Review ORCH-075 — ORCH-52b: стандарт документов конвейера
## Summary
Docs-only задача: создан golden source структуры номерных документов (`docs/_standards/PIPELINE_DOCS.md`),
15 копируемых шаблонов (`docs/_templates/*`), зафиксирована конвенция ADR-naming, заведён сквозной
ADR `adr-0019`, обновлены точки-ссылки (CLAUDE.md, architecture/README.md, CHANGELOG.md).
Манифест и шаблоны **сверены с фактическим кодом** — соответствие подтверждено. Все 7 критериев
приёмки выполнены. P0/P1/P2 findings нет → **APPROVED**.
## Оси проверки
### 1. Соответствие ТЗ (02-trz.md)
- FR-1 (манифест) — таблица покрывает весь реальный набор `00/01/02/03/04/06/07/08/10/12/13/14/15/16/17`,
колонки владелец/категория/стадия/гейт/machine-key присутствуют. ✓
- FR-2 (шаблоны) — все 15 шаблонов созданы; секции совпадают с FR-2.1 (спот-чек 01-brd, 02-trz,
06-adr, 04-test-plan). ✓
- FR-3 (ADR-naming) — §4 фиксирует путь, `ADR-NNN-<kebab-slug>`, связь с глобальным реестром, примеры. ✓
- FR-4 (точки-ссылки) — CLAUDE.md (раздел «Артефакты задачи» + правило 2), README §«Стандарт
документов конвейера», CHANGELOG `## [Unreleased]` (`docs`-тип). ✓
### 2. Соответствие ADR (06-adr/ADR-001 + adr-0019)
- D2 «манифест документирует, источник истины — код» отражён в самом манифесте (блок «Статус истины»). ✓
- D5 ground-truth сверка соответствует тому, что реально читает код (проверено независимо). ✓
- Стандарт следует собственной конвенции (заведён `adr-0019`). ✓
### 3. Качество кода (docs-only) — сверка с `src/`
Независимо подтверждено по источнику истины:
- `STAGE_TRANSITIONS` (`src/stages.py`) — рёбра и exit-гейты совпадают с манифестом 1:1.
- Frontmatter-ключи совпадают с парсерами: `verdict:``check_reviewer_verdict`; `result:`/`verdict:`/
`status:``_parse_tests_verdict`; `deploy_status:``_parse_deploy_status`; `staging_status:`
`_parse_staging_status`; `security_status:``check_security_gate`/`security_gate.py`.
- `check_analysis_complete` (01/02/03/04) и `check_architecture_done` (06-adr ≥1 файл ИЛИ 07-infra) —
формулировки манифеста точны.
- Под-гейты ребра `deploy-staging→deploy` корректно помечены как врезки в `advance_stage`, не строки
`STAGE_TRANSITIONS` (AC-7).
- AC-6: `git diff` по `src/` пуст — код/гейты/схема БД не тронуты.
### 4. Качество тестов
`tests/test_orch_52b_docs_standard.py` — 20 содержательных структурных тестов (наличие манифеста,
покрытие всех доков, владельцы/категории, frontmatter-ключи каждого машинного шаблона, ADR-naming
против реального репо, валидность YAML тест-плана, точки-ссылки, CHANGELOG). Прогон: **20 passed**.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
### P3 — Nice-to-have
- [ ] В `06-adr/ADR-001` §D4 формулировка «реестр идёт до `adr-0018`» описывает состояние ДО добавления
текущего `adr-0019` (что верно), тогда как `PIPELINE_DOCS.md` §4 говорит «доходит до `adr-0019`».
Несоответствие безвредно (разные срезы времени), правка не требуется.
## Документация
Это docs-only задача — документация **является** деливерейблом. `src/` не изменён, поэтому правило
CLAUDE.md «изменил src → обнови доку» неприменимо в блокирующем смысле. Сама документация проверена на
достоверность против кода (`src/stages.py`, `src/qg/checks.py`, `src/security_gate.py`) и эталонных
доков — расхождений нет. Точки-онбординга (CLAUDE.md, architecture/README.md) и CHANGELOG обновлены.
Статус документации: **полностью обновлена и верифицирована**.

View File

@@ -0,0 +1,85 @@
---
type: test-report
work_item_id: ORCH-075
result: PASS
---
# Test Report — ORCH-075 (ORCH-52b: стандарт документов конвейера)
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Дата: 2026-06-09
- Ветка: `feature/ORCH-075-orch-52b-docs-templates-adr-na`
- Prod health (`http://localhost:8500/health`): `{"status":"ok","service":"orchestrator"}`
- Review verdict (12-review.md): **APPROVED** (предусловие выполнено)
## Smoke-тест API (read-only, прод не трогался)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
| `GET /status` | OK — активная задача ORCH-075 (id 68) на стадии `testing` |
| `GET /queue` | OK — counts {running:1, done:871, failed:4}, breaker `closed`, reconcile/reaper enabled |
## Результаты
### Полный регресс
`python -m pytest tests/ -q`**1177 passed, 1 warning in 38.08s** (warning — Pydantic V2 deprecation в `src/config.py`, не относится к задаче). Регресса от docs-изменения нет.
### Профильная сюита
`python -m pytest tests/test_orch_52b_docs_standard.py -v`**20 passed in 0.39s**.
### Сопоставление с тест-планом (04-test-plan.yaml)
| TC ID | Описание | Результат |
|-------|----------|-----------|
| TC-01 | PIPELINE_DOCS.md существует и непустой | PASS |
| TC-02 | Манифест упоминает все номерные доки (00..17) | PASS |
| TC-03 | Манифест указывает владельца-агента для каждого дока | PASS |
| TC-04 | Манифест содержит категории required/when-applicable/optional | PASS |
| TC-05 | docs/_templates/ содержит шаблоны всех required/when-applicable доков | PASS |
| TC-06 | Шаблон 12-review содержит `verdict:` | PASS |
| TC-07 | Шаблон 13-test-report содержит `result:` | PASS |
| TC-08 | Шаблон 14-deploy-log содержит `deploy_status:` | PASS |
| TC-09 | Шаблон 15-staging-log содержит `staging_status:` | PASS |
| TC-10 | Шаблон 17-security-report содержит `security_status:` | PASS |
| TC-11 | Шаблон 16-post-deploy-log содержит `post_deploy_status:` | PASS |
| TC-12 | Шаблон 01-brd содержит обязательные секции | PASS |
| TC-13 | Шаблон 02-trz содержит обязательные секции | PASS |
| TC-14 | Шаблон 03-acceptance-criteria содержит блок AC-N с PASS/FAIL | PASS |
| TC-15 | Шаблон 04-test-plan.yaml — валидный YAML с work_item/tests | PASS |
| TC-16 | Раздел ADR-naming фиксирует формат ADR-NNN-<slug>.md (с 001, kebab) | PASS |
| TC-17 | ADR-naming совпадает с реальными ADR в репо | PASS |
| TC-18 | CLAUDE.md ссылается на docs/_standards/PIPELINE_DOCS.md | PASS |
| TC-19 | docs/architecture/README.md ссылается на стандарт | PASS |
| TC-20 | CHANGELOG.md содержит запись ORCH-52b/ORCH-075 в Unreleased | PASS |
| TC-21 | Регресс: полный прогон pytest tests/ зелёный | PASS |
### Сопоставление с критериями приёмки (03-acceptance-criteria.md)
| AC | Критерий | Результат |
|----|----------|-----------|
| AC-1 | Манифест создан, покрывает весь набор + владелец/категория | PASS (TC-01..04) |
| AC-2 | Шаблоны для каждого required/when-applicable + frontmatter-ключи + секции | PASS (TC-05..14) |
| AC-3 | ADR-naming зафиксирован | PASS (TC-16) |
| AC-4 | Согласованность с эталонами и кодом | PASS (TC-15,17; reviewer сверил с src/) |
| AC-5 | Ссылки + CHANGELOG обновлены | PASS (TC-18..20) |
| AC-6 | Код гейтов НЕ изменён (docs-only) | PASS — `git diff origin/main...HEAD -- src/` пуст; затронуты только `docs/**`, `CLAUDE.md`, `CHANGELOG.md`, `tests/test_orch_52b_docs_standard.py` |
| AC-7 | Манифест различает machine-verdict и информационные доки | PASS (reviewer подтвердил врезки `advance_stage` и разметку гейтов) |
## Вывод pytest
```
........................................................................ [ 97%]
......................... [100%]
=============================== warnings summary ===============================
src/config.py:5: PydanticDeprecatedSince20: ...
1177 passed, 1 warning in 38.08s
```
```
tests/test_orch_52b_docs_standard.py — 20 passed, 1 warning in 0.39s
```
## Итог
**PASS** — полный регресс зелёный (1177 passed), профильная сюита зелёная (20 passed),
smoke API OK, изменение строго docs-only (AC-6 подтверждён: `src/` не тронут).
Задача готова к стадии `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-075
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,49 @@
---
staging_status: SUCCESS
timestamp: 2026-06-09T10:22:40Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (8501).
Result: **8/10 checks PASS**, exit code **0**`staging_status: SUCCESS`.
All REAL (pipeline) checks are green. The only two failures are the known
sandbox-infra checks **C9a / C9b**, which depend on SANDBOX bot accounts being
project members (infra precondition), not on the pipeline. Per ORCH-061 they are
tolerated when every REAL check is green; the suite printed an `INFRA-WAIVED:` line
and exited 0 (fail-closed for real checks preserved).
## Execution
- Command: `python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
- Ran inside the `orchestrator-staging` container (canonical, ADR-001 / ORCH-048),
so the B6 registry-isolation check reads the running instance's own process-env.
- Note: the `docker` CLI is not installed in this environment; the exec was issued
through the mounted Docker Engine API socket (`/var/run/docker.sock`), which is
functionally equivalent to `docker exec orchestrator-staging …`.
## Observability — waiver line (ORCH-061)
```
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 summary
| 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 (SANDBOX_INFRA, waived) |
| C E2E | C9b Analyst job enqueued in staging queue | ✗ FAIL (SANDBOX_INFRA, waived) |
REAL failed: none. SANDBOX_INFRA failed: C9a, C9b (waived). Exit code: 0.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
Work Item ID: ORCH-076
## Description
TBD

View File

@@ -0,0 +1,151 @@
# 01 — BRD (бизнес-требования): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
## 1. Бизнес-контекст и проблема
Это **слой 2 эпика ORCH-52** (стандартизация документного конвейера). Слой 1 (ORCH-52b /
ORCH-075) уже в `main`: создан **описательный** стандарт `docs/_standards/PIPELINE_DOCS.md`
+ копируемые скелеты `docs/_templates/*`. Стандарт честно фиксирует карту «стадия → агент →
документ → гейт → frontmatter machine-key», но прямо помечен как слой описательный:
«Машинная проверка соответствия шаблонам/frontmatter — отдельная задача ORCH-52c».
Установленные факты (проверено в репо на ветке задачи):
- **`src/frontmatter.py` = ТОЛЬКО reader.** Единственная функция
`read_frontmatter_value(path, key) -> str | None` (single-key, ~2.6 KB). В docstring
модуля прямой коммент: *«merging into a single parser is a follow-up task»* — это и есть
ORCH-52c. Контракт reader — **never raises** (любая ошибка → `None` + `logger.debug`).
- **Протокол вердиктов размазан по отдельным парсерам с дублированной ~10-строчной
YAML-frontmatter-логикой:**
- `src/qg/checks.py::check_reviewer_verdict` — читает `verdict:` из `12-review.md`;
- `src/qg/checks.py::_parse_tests_verdict` — читает `result:`/`verdict:`/`status:` из
`13-test-report.md` (три равноранговых поля, ORCH-047);
- `src/qg/checks.py::_parse_deploy_status` — читает `deploy_status:` из `14-deploy-log.md`;
- `src/qg/checks.py::_parse_staging_status` — читает `staging_status:` из `15-staging-log.md`;
- `src/security_gate.py::parse_security_status` — читает `security_status:` из `17-security-report.md`;
- `src/post_deploy.py` — пишет/читает `post_deploy_status:` в `16-post-deploy-log.md`;
- `src/review_parse.py` — defensive-извлечение прозы (`_strip_frontmatter`).
Каждый парсер заново реализует `content.startswith("---")``split("---", 2)`
`yaml.safe_load`. Единого контракта нет → риск рассинхрона (разная обработка ошибок,
разный набор токенов, разный регистр).
- **Нет формальной спеки handoff:** нигде не зафиксировано «что КАЖДАЯ стадия ОБЯЗАНА
оставить на выходе» (полный список артефактов + обязательные frontmatter-ключи) как
единый контракт передачи между стадиями.
**Боль/риск:** без единого контракта чтения вердиктов и без обязательной схемы frontmatter
каждая правка одного парсера может разойтись с остальными; новый агентский документ легко
написать с неверным ключом/регистром (гейт упадёт ложно), а отсутствие машинной проверки
схемы оставляет соблюдение стандарта на ручную дисциплину reviewer'а.
**⚠️ Self-hosting.** Задача меняет КОД, читающий вердикты НА ГЕЙТАХ (review/staging/security/
tester/deploy) в инструменте, который сейчас обслуживает прод (enduro-trails) из общего
инстанса. Любой регресс чтения вердикта = остановка конвейера всех проектов. Поэтому
рефакторинг обязан быть строго обратно совместимым и fail-safe.
## 2. Объём (scope)
### В объёме
- **Спека handoff** в `docs/_standards/` (рядом с `PIPELINE_DOCS.md`): формальный контракт
«стадия → обязательный выход» (какие документы + какие frontmatter-ключи обязательны на
выходе каждой стадии), согласованный с манифестом ORCH-52b.
- **Расширение `src/frontmatter.py`:** к существующему reader добавить **writer** (запись
YAML-frontmatter) и **валидатор** обязательной схемы. Обязательная схема:
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`.
- **Единый контракт вердиктов в одном месте** (док + единый frontmatter-API): гейты
(reviewer→`verdict:`, tester→`result:`, deployer→`deploy_status:`, staging→`staging_status:`,
security→`security_status:`) читают СТАНДАРТНЫЕ поля через единый frontmatter-API, а не
через разрознённые ad-hoc парсеры.
- Обновление документации (CLAUDE.md, architecture/README, ADR — глобальный и per-work-item,
CHANGELOG).
### Вне объёма
- **Правка промптов агентов** (`.openclaw/agents/*.md`), чтобы те эмитили новую полную схему
— это **ORCH-52d** (слой 3).
- **Ретро-фит старых документов** (дописывание новой схемы в уже существующие work-items).
- Изменение `STAGE_TRANSITIONS` и **состава** `QG_CHECKS` (какие гейты существуют).
- Изменение **семантики** вердиктов (какое значение → какой переход) — только КАК они
читаются.
- Включение hard-fail валидации схемы по умолчанию (дефолт — warning; hard-fail только под
явно включённым kill-switch).
## 3. Заинтересованные стороны
- **Заказчик / Owner** — Слава (homenet542): подтверждает BRD (ручной гейт остаётся ручным).
- **Самообслуживаемый инструмент (self-hosting)** — оркестратор правит сам себя; задача —
первый боевой тест `autoDeploy` (см. примечание ниже).
- **Затрагиваемые роли конвейера** — reviewer / tester / deployer / security-гейт (их
вердикты теперь читаются через единый API); architect/analyst (новая обязательная схема
для будущих документов, фактическое внедрение — ORCH-52d).
- **Другие проекты (enduro-trails)** — НЕ должны почувствовать изменений (нулевая регрессия).
## 4. Бизнес-требования (BR)
- **BR-1** — `src/frontmatter.py` предоставляет полный набор операций над YAML-frontmatter:
**reader** (сохранён без изменения контракта), **writer** (сериализация frontmatter в
документ), **валидатор** (проверка обязательной схемы).
- **BR-2** — Обязательная схема frontmatter определена и проверяема: поля `work_item`,
`stage`, `author_agent`, `status`, `created_at`, `model_used`.
- **BR-3** — Создана формальная спека handoff в `docs/_standards/`, согласованная с
`PIPELINE_DOCS.md`: для каждой стадии указано, какие документы и какие frontmatter-ключи
она обязана оставить на выходе.
- **BR-4** — Контракт вердиктов сведён в ОДНО место; все пять гейтов-вердиктов
(review/staging/security/tester/deploy) читают стандартные поля через единый
frontmatter-API, а не через разрознённые парсеры.
- **BR-5** — Семантика вердиктов неизменна: то же значение → тот же переход/откат, что и
сейчас (включая трёх-полевой контракт tester'а ORCH-047 и токен-логику BLOCKED/FAILED).
## 5. Нефункциональные требования (NFR)
- **NFR-1 (обратная совместимость, критично self-hosting)** — Существующие документы-вердикты
БЕЗ новой полной схемы ПРОДОЛЖАЮТ читаться гейтами (fallback на текущее поведение). Старый
`12/13/14/15/17`-док без `work_item/stage/...` парсится по вердикт-ключу как раньше.
- **NFR-2 (never-raise / fail-safe)** — Ошибка writer'а или валидатора НЕ роняет конвейер
(тот же контракт, что у reader: любая ошибка → лог + безопасное значение, исключение
наружу не выходит).
- **NFR-3 (валидатор не self-block)** — Валидатор обязательной схемы НЕ является hard-fail
на гейте по умолчанию (иначе сама ORCH-52c заблокировала бы себя на собственном деплое,
т.к. её документы и документы соседей ещё без полной схемы). Дефолт — warning/лог;
жёсткость — под kill-switch (флаг).
- **NFR-4 (нулевая регрессия для enduro)** — Поведение для не-self-hosting репозиториев и
всех существующих гейтов остаётся 1:1; полный регресс `tests/` зелёный.
- **NFR-5 (обратимость)** — Поведенческие изменения (если есть, напр. строгая валидация)
закрываются kill-switch с дефолтом, эквивалентным прежнему поведению.
## 6. Допущения и ограничения
- Frontmatter везде в каноне — ведущий YAML-блок между `---``---` (как в `qg/checks.py`
и `frontmatter.py`).
- Источник истины о поведении гейтов остаётся КОД (`src/stages.py`, `src/qg/checks.py`,
`src/stage_engine.py`); спека/манифест документируют, а не управляют (правило ORCH-075).
- `model_used` в схеме — это модель, которой документ создан; фактический источник значения
для агентских доков — резолв `resolve_agent_model` (ORCH-41); проставление в реальные
документы агентами — ORCH-52d, вне scope.
- `pyyaml` уже зависимость проекта (используется во всех существующих парсерах).
- Реализационные решения (одна функция-парсер vs класс, точная сигнатура writer/валидатора,
имя модуля контракта вердиктов) — прерогатива архитектора (06-adr), здесь не предрешаются.
## 7. Критерии успеха
Задача успешна, если: `src/frontmatter.py` несёт reader+writer+валидатор обязательной схемы;
спека handoff создана и согласована с `PIPELINE_DOCS.md`; все пять гейтов-вердиктов читают
через единый frontmatter-API; старые доки-вердикты продолжают проходить гейты (анти-регресс);
ошибка writer/валидатора не роняет конвейер, hard-fail валидации под kill-switch (дефолт —
warning); `STAGE_TRANSITIONS` и состав `QG_CHECKS` не изменены, семантика вердиктов неизменна;
документация обновлена; **сама ORCH-52c проходит свои гейты** (включая первый боевой
`autoDeploy`). Детальные PASS/FAIL — `03-acceptance-criteria.md`.
## 8. Риски
- **Регресс чтения вердикта на гейте** → остановка конвейера всех проектов (главный риск
self-hosting). Митигация — строгая обратная совместимость + полный регресс тестов гейтов.
- **Самоблокировка валидатором** на собственном деплое (документы без полной схемы).
Митигация — NFR-3 (валидатор не hard-fail по умолчанию).
- **Расхождение спеки handoff с фактом кода** → «лживый» стандарт. Митигация — согласование
с `PIPELINE_DOCS.md` и явная пометка «источник истины — код».
- **Первый боевой `autoDeploy`** — авто-подтверждение прод-деплоя орка (см. примечание).
Детали митигации/наблюдения — задача архитектора (`10-tech-risks.md`).
> **Примечание (АВТО-ДЕПЛОЙ).** На этой задаче выставлен лейбл `autoDeploy` (ORCH-089): орк
> САМ подтверждает прод-деплой после зелёного staging + всех тех-гейтов. BRD-гейт остаётся
> ручным (Слава подтверждает BRD). Это первый боевой тест `autoDeploy`.

View File

@@ -0,0 +1,124 @@
# 02 — ТЗ (TRZ): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное обоснование/решения (как именно структурировать модуль контракта вердиктов,
> точные сигнатуры) — задача архитектора (06-adr).
## 1. Сводка изменения
ORCH-52c превращает `src/frontmatter.py` из single-key reader в полный frontmatter-контракт
(**reader + writer + валидатор обязательной схемы**) и сводит **разрознённое чтение вердиктов**
гейтов к **единому frontmatter-API**, не меняя ни состав гейтов, ни семантику вердиктов.
Дополнительно создаётся **формальная спека handoff** в `docs/_standards/`, согласованная с
манифестом ORCH-52b (`PIPELINE_DOCS.md`). Всё строго обратно совместимо (старые доки читаются
как раньше), never-raise, валидатор не hard-fail по умолчанию (kill-switch).
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `src/frontmatter.py` | **изменить** — добавить writer + валидатор + чтение всего frontmatter (multi-key/dict); reader `read_frontmatter_value` сохранить (контракт неизменен) |
| `src/qg/checks.py` | **изменить**`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status` перевести на чтение через единый frontmatter-API (поведение/токены/семантика 1:1) |
| `src/security_gate.py` | **изменить**`parse_security_status` читает `security_status:` через единый API (семантика 1:1) |
| `src/post_deploy.py` | **изменить (по решению архитектора)** — чтение `post_deploy_status:` через единый API (информационный, не гейт) |
| `src/review_parse.py` | **возможно изменить**`_strip_frontmatter` может использовать общий хелпер; контракт «never raise → ""» сохранить |
| `src/config.py` | **изменить** — добавить kill-switch строгой валидации (напр. `frontmatter_validation_strict: bool = False`) |
| `docs/_standards/HANDOFF_PROTOCOL.md` (имя — на усмотрение архитектора/стандарта) | **создать** — формальная спека handoff «стадия → обязательный выход» |
| `docs/_standards/PIPELINE_DOCS.md` | **изменить** — связать со спекой handoff, отметить что ORCH-52c реализовала машинный контракт |
| `tests/test_frontmatter.py` | **создать** — unit на reader/writer/валидатор/round-trip |
| `tests/` (гейты) | **изменить/создать** — анти-регресс тесты чтения вердиктов через новый API |
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`, ADR | **изменить/создать** — документация |
## 3. Функциональные требования
### FR-1 — Writer frontmatter (BR-1)
В `src/frontmatter.py` добавить функцию записи: принимает данные frontmatter (mapping
ключ→значение) и тело документа, возвращает/записывает строку с каноничным ведущим
YAML-блоком `---\n…\n---\n<body>`. Формат на 100% совместим с существующими парсерами
(`split("---", 2)` + `yaml.safe_load`). **never-raise** (NFR-2): ошибка сериализации/записи →
лог + безопасный результат, исключение наружу не выходит. Точная сигнатура (in-memory render
vs запись в файл, перезапись существующего frontmatter) — решение архитектора.
### FR-2 — Валидатор обязательной схемы (BR-2, NFR-3)
В `src/frontmatter.py` добавить валидатор, проверяющий наличие обязательных полей схемы:
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`. Возвращает
структурированный результат (список отсутствующих/невалидных полей + признак валидности).
**Поведение по умолчанию — warning/лог, НЕ blocker** (NFR-3): отсутствие полей не роняет
конвейер и не заваливает гейт. Жёсткость (hard-fail) включается ТОЛЬКО kill-switch'ем
`frontmatter_validation_strict` (дефолт `False`). never-raise.
### FR-3 — Полночтение frontmatter / единый reader-API (BR-1, BR-4)
В `src/frontmatter.py` добавить чтение ВСЕГО frontmatter как mapping (а не только single-key),
поверх которого строится единый доступ к вердикт-полям. Существующий
`read_frontmatter_value(path, key)` сохраняется без изменения контракта (обратная
совместимость вызывающих — `notifications.build_status_comment` и т.п.). never-raise.
### FR-4 — Единый контракт чтения вердиктов (BR-4, BR-5, NFR-1)
Пять гейтов-вердиктов читают свои стандартные поля через единый frontmatter-API:
| Гейт / парсер | Документ | Стандартное поле | Семантика (НЕИЗМЕННА) |
|---------------|----------|------------------|------------------------|
| `check_reviewer_verdict` | `12-review.md` | `verdict:` | `APPROVED`→дальше; `REQUEST_CHANGES`→откат на development |
| `_parse_tests_verdict` | `13-test-report.md` | `result:` / `verdict:` / `status:` (3 равноранговых, ORCH-047) | `PASS`→дальше; `FAIL`/`BLOCKED`→откат; негативный токен авторитетен |
| `_parse_deploy_status` | `14-deploy-log.md` | `deploy_status:` | `SUCCESS`→done; `FAILED`→откат (БАГ-8) |
| `_parse_staging_status` | `15-staging-log.md` | `staging_status:` | `SUCCESS`→дальше; `FAILED`→откат (self-hosting; иначе N/A) |
| `parse_security_status` | `17-security-report.md` | `security_status:` | `PASS`→дальше; `FAIL`→откат |
Требование: **только механизм чтения** унифицируется (одна точка парсинга YAML-frontmatter);
наборы токенов (`_TESTS_NEGATIVE_TOKENS`/`_TESTS_POSITIVE_TOKENS`), приведение к верхнему
регистру, обработка «no frontmatter / bad YAML / missing key», fallback `worktree → origin/main`
для deploy/staging — сохраняются 1:1. Возврат каждого `check_*` — прежний `tuple[bool, str]`.
### FR-5 — Обратная совместимость старых доков (NFR-1, критично)
Документ-вердикт БЕЗ новых полей схемы (`work_item/stage/author_agent/status/created_at/
model_used`), но с вердикт-ключом (`verdict:`/`result:`/`deploy_status:`/…) ДОЛЖЕН читаться
гейтом ровно как сейчас. Новая схема — аддитивна; её отсутствие не влияет на чтение вердикта.
### FR-6 — Спека handoff (BR-3)
Создать в `docs/_standards/` формальную спеку «стадия → обязательный выход»: для каждой стадии
(`created``analysis``architecture``development``review``testing``deploy-staging``deploy`
`done`) перечислить обязательные документы и обязательные frontmatter-ключи на выходе.
Согласовать с таблицей §2 `PIPELINE_DOCS.md` (тот же набор документов/ключей/гейтов), явно
указать «источник истины — код». Различать machine-verdict доки и информационные (как в
`PIPELINE_DOCS.md` §3).
## 4. Изменения API
Нет. HTTP-эндпоинты не добавляются/не меняются. (Опционально архитектор может предложить блок
наблюдаемости в `GET /queue` для счётчика валидации — НЕ требование данной задачи.)
## 5. Изменения схемы БД
Нет. Таблицы/миграции/индексы не затрагиваются. Контракт работает на файлах
(YAML-frontmatter) и in-memory.
## 6. Требования к новым/изменённым QG checks
- **Состав `QG_CHECKS` НЕ изменяется** (никаких новых/удалённых зарегистрированных гейтов) —
AC-6 / правило CLAUDE.md.
- Изменяется только **внутренняя реализация чтения вердикта** существующих `check_*`/`_parse_*`
(делегирование единому frontmatter-API). Сигнатуры и возвращаемые значения (`tuple[bool,str]`)
— неизменны.
- Новый kill-switch `frontmatter_validation_strict` (config) управляет жёсткостью валидатора
схемы; дефолт `False` (warning-only) → нулевая поведенческая регрессия.
## 7. Совместимость / регресс
- **Обратная совместимость (NFR-1):** старые доки-вердикты без новой схемы читаются как
раньше; контракт `read_frontmatter_value` неизменен; формат writer'а совместим с
существующими парсерами.
- **never-raise (NFR-2):** writer/валидатор/единый reader не выбрасывают исключений в
конвейер (паттерн текущего `frontmatter.py`).
- **kill-switch / обратимость (NFR-3, NFR-5):** `frontmatter_validation_strict=False` (дефолт)
→ валидация только логирует; `True` → строгий режим (на будущее). Поведение деградирует к
прежнему при дефолтном флаге.
- **Неизменность контрактов (AC-6):** `STAGE_TRANSITIONS`, состав `QG_CHECKS`, семантика
вердиктов, fallback `worktree→origin/main`, трёх-полевой контракт tester (ORCH-047),
токен-логика BLOCKED/FAILED — без изменений.
- **Нулевая регрессия enduro (NFR-4):** для не-self-hosting репо поведение 1:1; условные гейты
(ORCH-35/43/58) не затрагиваются по существу.
- **Полный регресс `tests/` зелёный** перед мержем.
- **self-hosting:** не перезапускать прод-контейнер вручную; деплой через штатный путь;
первый боевой `autoDeploy` (наблюдение — за стадией deploy).

View File

@@ -0,0 +1,104 @@
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам
репозитория. Критерии прямо отражают AC из постановки задачи (AC-1…AC-7).
---
## AC-1 — frontmatter: reader + writer + валидатор
**Условие:** `src/frontmatter.py` несёт полный контракт.
- **PASS:** в `src/frontmatter.py` есть (а) сохранённый reader `read_frontmatter_value` с
прежним контрактом; (б) **writer** (запись/рендер YAML-frontmatter); (в) **валидатор**
обязательной схемы, проверяющий поля `work_item`, `stage`, `author_agent`, `status`,
`created_at`, `model_used`. Все три покрыты unit-тестами.
- **FAIL:** отсутствует writer ИЛИ валидатор; или валидатор не проверяет полный список из
6 обязательных полей; или контракт reader сломан (изменена сигнатура/поведение).
---
## AC-2 — спека handoff создана и согласована
**Условие:** формальный контракт handoff в `docs/_standards/`.
- **PASS:** в `docs/_standards/` создан документ-спека, где для КАЖДОЙ стадии указано, какие
документы и какие frontmatter-ключи она обязана оставить на выходе; набор документов/ключей/
гейтов согласован с `PIPELINE_DOCS.md` §2§3 (нет противоречий); `PIPELINE_DOCS.md`
обновлён ссылкой на спеку и отметкой о реализации машинного контракта в ORCH-52c.
- **FAIL:** спека отсутствует, не в `docs/_standards/`, покрывает не все стадии, или
противоречит `PIPELINE_DOCS.md` (другой набор ключей/документов).
---
## AC-3 — единый контракт вердиктов
**Условие:** гейты читают вердикты через единый frontmatter-API.
- **PASS:** контракт вердиктов сведён в ОДНО место (единый frontmatter-API); все пять
вердикт-точек — `check_reviewer_verdict` (`verdict:`), `_parse_tests_verdict`
(`result:`/`verdict:`/`status:`), `_parse_deploy_status` (`deploy_status:`),
`_parse_staging_status` (`staging_status:`), `parse_security_status` (`security_status:`) —
парсят YAML-frontmatter через этот API, а не дублированной ad-hoc логикой.
- **FAIL:** хотя бы один из пяти гейтов по-прежнему содержит собственную дублированную
реализацию парсинга YAML-frontmatter вместо единого API.
---
## AC-4 — анти-регресс: старые доки читаются, ORCH-52c проходит свои гейты (критично self-hosting)
**Условие:** обратная совместимость + самопрохождение.
- **PASS:** документ-вердикт БЕЗ новой полной схемы (только с вердикт-ключом) читается гейтом
ровно как до задачи (подтверждено тестом для каждого из пяти гейтов); полный регресс
`tests/` зелёный; **сама ORCH-52c проходит свои гейты** (review→testing→staging→deploy)
и доезжает до `done`.
- **FAIL:** любой старый док-вердикт перестал читаться/изменил вердикт; регресс `tests/`
красный; задача застряла/откатилась на собственном гейте из-за нового контракта.
---
## AC-5 — never-raise + валидатор не hard-fail по умолчанию (kill-switch)
**Условие:** fail-safe и не-самоблокирующая валидация.
- **PASS:** ошибка writer'а/валидатора логируется и НЕ роняет конвейер (исключение наружу не
выходит, подтверждено тестом на битом вводе); валидация обязательной схемы по умолчанию —
warning/лог, НЕ blocker; hard-fail доступен ТОЛЬКО под kill-switch
(`frontmatter_validation_strict`, дефолт `False`).
- **FAIL:** ошибка writer/валидатора пробрасывается в конвейер; ИЛИ отсутствие полей схемы
по умолчанию заваливает гейт/останавливает задачу; ИЛИ нет kill-switch для строгого режима.
---
## AC-6 — STAGE_TRANSITIONS и состав QG_CHECKS не изменены; семантика неизменна
**Условие:** инварианты конвейера.
- **PASS:** `src/stages.py::STAGE_TRANSITIONS` и реестр `QG_CHECKS` (`src/qg/checks.py`) —
без изменений по составу (те же стадии, те же зарегистрированные гейты); семантика каждого
вердикта (значение → переход/откат) идентична прежней, включая ORCH-047 (3 равноранговых
поля tester) и приоритет негативного токена.
- **FAIL:** изменён состав `STAGE_TRANSITIONS`/`QG_CHECKS`; или хоть один вердикт даёт другой
переход при том же значении, что до задачи.
---
## AC-7 — документация обновлена
**Условие:** golden-source документации синхронна с кодом.
- **PASS:** обновлены `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`; заведён
ADR per-work-item (`docs/work-items/ORCH-076/06-adr/ADR-001-*.md`) и сквозной
(`docs/architecture/adr/adr-NNNN-*.md`); спека handoff и `PIPELINE_DOCS.md` согласованы.
- **FAIL:** функционал изменён, но доки/ADR/CHANGELOG не обновлены (reviewer →
REQUEST_CHANGES по правилу CLAUDE.md №6).
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / BR-2 / FR-1 / FR-2 / FR-3 |
| AC-2 | BR-3 / FR-6 |
| AC-3 | BR-4 / FR-4 |
| AC-4 | NFR-1 / NFR-4 / FR-5 |
| AC-5 | NFR-2 / NFR-3 / NFR-5 / FR-2 |
| AC-6 | BR-5 / NFR-1 |
| AC-7 | правило CLAUDE.md №2/№6 |

View File

@@ -0,0 +1,122 @@
work_item: ORCH-076
title: "ORCH-52c — handoff-протокол + frontmatter writer/валидатор/единый контракт вердиктов"
framework: pytest
scope: >
Покрывается: writer/валидатор/единое чтение frontmatter (src/frontmatter.py);
чтение пяти гейтов-вердиктов через единый API при семантике 1:1; обратная
совместимость старых доков без новой схемы; never-raise; kill-switch строгой
валидации. Вне покрытия: правка промптов агентов (ORCH-52d), ретро-фит старых
документов, изменение STAGE_TRANSITIONS/состава QG_CHECKS.
notes: >
Полный регресс tests/ должен оставаться зелёным (анти-регресс гейтов, AC-4/AC-6).
Регресс = любой существующий тест гейтов (review/tester/deploy/staging/security),
ставший красным, или изменение вердикта при том же входном значении.
Тесты не должны требовать сети (frontmatter — файловый/in-memory контракт).
tests:
# --- frontmatter.py: writer / валидатор / reader (AC-1, AC-5) ---
- id: TC-01
type: unit
description: "Writer сериализует mapping в каноничный ведущий YAML-frontmatter (--- ... ---), читаемый существующими парсерами"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-02
type: unit
description: "Round-trip: writer записал frontmatter -> reader read_frontmatter_value возвращает те же значения по ключам"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-03
type: unit
description: "Валидатор: полная схема (work_item/stage/author_agent/status/created_at/model_used) -> valid=True, нет отсутствующих полей"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-04
type: unit
description: "Валидатор: отсутствие части обязательных полей -> valid=False со списком отсутствующих, но БЕЗ исключения (warning-only по умолчанию)"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-05
type: unit
description: "never-raise: writer и валидатор на битом вводе (None/не-mapping/нечитаемый путь/битый YAML) не выбрасывают исключение, возвращают безопасное значение + лог"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-06
type: unit
description: "reader read_frontmatter_value сохраняет прежний контракт (single-key, None на ошибку/отсутствие, strip, регистр сохранён)"
module: tests/test_frontmatter.py
expected: PASS
- id: TC-07
type: unit
description: "kill-switch frontmatter_validation_strict: False -> отсутствие полей не блокирует; True -> строгий режим сигнализирует невалидность"
module: tests/test_frontmatter.py
expected: PASS
# --- единый контракт вердиктов: чтение через общий API, семантика 1:1 (AC-3, AC-6) ---
- id: TC-08
type: unit
description: "check_reviewer_verdict через единый API: verdict: APPROVED -> (True); REQUEST_CHANGES -> (False); отсутствие -> (False) — как до задачи"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-09
type: unit
description: "_parse_tests_verdict через единый API: ORCH-047 три равноранговых поля (result/verdict/status), приоритет негативного токена (BLOCKED/FAILED) сохранён"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-10
type: unit
description: "_parse_deploy_status через единый API: deploy_status SUCCESS -> (True); FAILED -> (False); missing/bad YAML -> (False) — семантика БАГ-8 неизменна"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-11
type: unit
description: "_parse_staging_status через единый API: SUCCESS/FAILED семантика и условность ORCH-35 (non-self -> N/A pass) сохранены"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-12
type: unit
description: "parse_security_status через единый API: security_status PASS -> (True); FAIL -> (False) — семантика неизменна"
module: tests/test_security_gate.py
expected: PASS
# --- обратная совместимость / анти-регресс (AC-4) ---
- id: TC-13
type: unit
description: "Старый док-вердикт БЕЗ новой схемы (только verdict/result/deploy_status/staging_status/security_status) читается каждым из пяти гейтов как до задачи"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-14
type: unit
description: "Док С новой полной схемой + вердикт-ключом читается гейтом с тем же вердиктом, что и без схемы (схема аддитивна, не влияет на вердикт)"
module: tests/test_qg_verdicts.py
expected: PASS
- id: TC-15
type: integration
description: "fallback worktree -> origin/main для check_deploy_status/check_staging_status сохранён при чтении через единый API"
module: tests/test_qg_verdicts.py
expected: PASS
# --- инварианты конвейера (AC-6) ---
- id: TC-16
type: unit
description: "Состав QG_CHECKS и STAGE_TRANSITIONS не изменён (тот же набор ключей/стадий, что эталон)"
module: tests/test_stages_invariants.py
expected: PASS
# --- полный регресс ---
- id: TC-17
type: integration
description: "Полный прогон tests/ зелёный (нет регресса существующих тестов гейтов и конвейера)"
module: tests/
expected: PASS

View File

@@ -0,0 +1,248 @@
# ADR-001: Единый frontmatter-контракт (reader+writer+валидатор) и унификация чтения вердиктов
Work Item: **ORCH-076** (ORCH-52c, слой 2 эпика ORCH-52) · Repo: **orchestrator** · Стадия: architecture
Дата: 2026-06-09 · Статус: **Accepted**
> Сквозная версия — [`docs/architecture/adr/adr-0020-frontmatter-contract.md`](../../../architecture/adr/adr-0020-frontmatter-contract.md).
---
## Статус
Accepted
## Контекст
(Подробно — `01-brd.md` §1, `02-trz.md`.) Слой 1 эпика (ORCH-075/52b) дал **описательный**
стандарт `docs/_standards/PIPELINE_DOCS.md`. ORCH-52c — **машинный** слой. Установлено в коде
на ветке задачи:
- `src/frontmatter.py` = **только reader** (`read_frontmatter_value(path, key) -> str | None`,
never-raise → `None`). В docstring прямой коммент: *«merging into a single parser is a
follow-up task»* — это и есть данная задача.
- **Парсинг YAML-frontmatter дублируется** в 5+ местах (~10 строк
`content.startswith("---")``split("---", 2)``yaml.safe_load``isinstance(dict)`):
`qg/checks.py::check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`,
`_parse_staging_status`; `security_gate.py::parse_security_status`; плюс `_strip_frontmatter`
в `review_parse.py` и `security_gate.extract_security_findings`. Каждый — своя обработка
ошибок и свои reason-строки → риск рассинхрона.
- **Нет машинно-проверяемой схемы** обязательного frontmatter и **нет формальной спеки
handoff** «что каждая стадия обязана оставить на выходе».
**⚠️ Self-hosting (главное ограничение проектирования).** Затрагиваемый код читает вердикты
**на гейтах** в инструменте, который прямо сейчас обслуживает прод (enduro-trails) из общего
инстанса с общей БД/очередью. Любой регресс чтения вердикта = остановка конвейера ВСЕХ
проектов. Рефакторинг обязан быть **строго обратно совместимым, never-raise, нулевая
регрессия**. Плюс на задаче выставлен лейбл `autoDeploy` (ORCH-089) — это **первый боевой
автодеплой** орка (детали риска — `10-tech-risks.md`).
## Движущие силы (требования)
BR-1…BR-5, NFR-1…NFR-5 (`01-brd.md`), FR-1…FR-6 (`02-trz.md`), AC-1…AC-7
(`03-acceptance-criteria.md`). Ключевые инварианты-ограничители:
- **INV-1** `STAGE_TRANSITIONS` и **состав** `QG_CHECKS` — не меняются (AC-6).
- **INV-2** Семантика каждого вердикта (значение → переход/откат) — 1:1, включая 3-полевой
контракт tester'а (ORCH-047) и приоритет негативного токена (AC-6, FR-4).
- **INV-3** Контракт `read_frontmatter_value` — неизменен (внешние вызыватели: `usage.py`,
`notifications.build_status_comment`) (FR-3).
- **INV-4** Валидатор схемы **не hard-fail по умолчанию** — иначе ORCH-52c заблокировала бы
собственный деплой (её доки и доки соседей ещё без полной схемы) (NFR-3).
- **INV-5** Никаких изменений API и схемы БД (TRZ §4§5).
---
## Решение
### D1. `src/frontmatter.py` становится единым frontmatter-контрактом (1 модуль, функции)
Выбран **набор функций в существующем leaf-модуле** (не класс, не новый пакет): модуль уже
есть, не зависит ни от чего проектного (только `logging` + ленивый `yaml`), импортируем без
циклов из `qg/checks.py`, `security_gate.py`, `post_deploy.py`, `review_parse.py`. Класс/состояние
не нужны — операции чистые. Это минимизирует blast radius (требование self-hosting).
**Публичный API (имена канонические; точные дефолты — в реализации, контракт фиксирован здесь):**
```python
# --- константы схемы ---
REQUIRED_FIELDS = ("work_item", "stage", "author_agent", "status", "created_at", "model_used")
# --- reader: СОХРАНЁН без изменения контракта (INV-3) ---
def read_frontmatter_value(path: str, key: str) -> str | None: ...
# --- единый парс-примитив (единственная точка YAML-логики) ---
@dataclass(frozen=True)
class FrontmatterParse:
data: dict # {} если нет/битый/не-mapping
has_block: bool # присутствовал ведущий ---…--- блок
malformed: bool # был "---", но < 3 сегментов (незакрытый блок)
yaml_error: str | None # текст ошибки yaml.safe_load, иначе None
def parse_frontmatter(content: str) -> FrontmatterParse: ... # never-raise
def parse_frontmatter_dict(content: str) -> dict: ... # ярлык → .data; never-raise → {}
def read_frontmatter(path: str) -> dict: ... # файл → parse; never-raise → {}
# --- writer ---
def render_frontmatter(data: Mapping[str, object], body: str = "") -> str: ...
# → "---\n<yaml>\n---\n<body>"; формат совместим со split("---",2)+safe_load; never-raise → body
def write_frontmatter(path: str, data: Mapping, body: str = "") -> bool: ...
# персист render_frontmatter; never-raise → False (ошибка логируется)
# --- валидатор схемы ---
@dataclass(frozen=True)
class SchemaValidation:
valid: bool
missing: list[str] # отсутствующие/пустые обязательные поля
def validate_schema(data: Mapping, *, required=REQUIRED_FIELDS) -> SchemaValidation: ... # never-raise
# --- общий хелпер тела (заменяет дубли _strip_frontmatter) ---
def strip_frontmatter(content: str) -> str: ... # never-raise → content
```
**Контракт всего модуля — never-raise** (NFR-2), как у действующего reader: любая ошибка
(I/O, YAML, сериализация) → `logger.debug/warning` + безопасное значение (`{}` / `False` /
исходный текст), исключение наружу **не выходит**.
`parse_frontmatter` возвращает **структуру** (а не голый dict), чтобы каждый гейт мог
**воспроизвести свои текущие reason-строки 1:1** (см. D2) — это и есть способ сохранить
семантику без переписывания сообщений (INV-2).
### D2. Унифицируется МЕХАНИЗМ парсинга, а НЕ семантика вердиктов
AC-3/FR-4 требуют «читать через единый frontmatter-API, а не дублированной ad-hoc логикой».
Унифицируется **ровно повторяющийся блок** `startswith/split/safe_load/isinstance`
замена на `parse_frontmatter(content)`. **Token-логика, upper-casing, набор полей, приоритет
негативного токена, fallback `worktree → origin/main` — остаются в каждом гейте без изменений.**
Это сознательное ограничение объёма унификации: общий «умный» verdict-резолвер увеличил бы
риск тонкого регресса на гейтах (недопустимо при self-hosting). Каждый `check_*`/`_parse_*`
сохраняет сигнатуру и `tuple[bool, str]`.
Маппинг состояний `FrontmatterParse` → существующие reason-строки (пример для tester'а,
остальные аналогично):
| Состояние | Прежняя ветка | Сохраняемая reason-строка |
|-----------|---------------|---------------------------|
| `not has_block` | `not content.startswith("---")` | "No YAML frontmatter in test report …" |
| `malformed` | `len(parts) < 3` | "Malformed YAML frontmatter in test report" |
| `yaml_error` | `except yaml.YAMLError` | "Invalid YAML frontmatter in test report: {e}" |
| `data` (dict) | `fm.get(...)` | прежняя token-логика поверх `parse.data` |
Точки перевода (FR-4):
| Парсер | Файл | Поле(я) | Семантика — НЕ менять |
|--------|------|---------|----------------------|
| `check_reviewer_verdict` | `12-review.md` | `verdict:` | APPROVED→дальше; REQUEST_CHANGES→откат |
| `_parse_tests_verdict` | `13-test-report.md` | `result:`/`verdict:`/`status:` (3 равноранг., ORCH-047) | PASS→дальше; FAIL/BLOCKED→откат; негативный токен авторитетен |
| `_parse_deploy_status` | `14-deploy-log.md` | `deploy_status:` | SUCCESS→done; FAILED→откат (БАГ-8) |
| `_parse_staging_status` | `15-staging-log.md` | `staging_status:` | SUCCESS→дальше; FAILED→откат (self-hosting) |
| `parse_security_status` | `17-security-report.md` | `security_status:` | PASS→дальше; FAIL→откат (FAIL авторитетен) |
`post_deploy.py` (`post_deploy_status:`, информационный) и `review_parse._strip_frontmatter`/
`security_gate.extract_security_findings` (извлечение прозы) переводятся на
`parse_frontmatter_dict` / `strip_frontmatter` соответственно — снимает оставшиеся дубли без
изменения их «never-raise → пусто» контрактов.
### D3. Валидатор: библиотека + warning-only, hard-fail строго под kill-switch
`validate_schema`**чистая библиотечная функция** (INV-4, NFR-3). Чтобы гарантировать
**нулевую регрессию гейтов**, в default-режиме валидатор **не участвует в вычислении
boolean-вердикта** ни одного гейта. Вместо этого:
- Новый флаг `config.frontmatter_validation_strict: bool = False`
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`).
- **Default (`False`):** опциональный warning-emit — при чтении machine-verdict дока, не
несущего полной схемы, единый хелпер `maybe_warn_schema(content, doc_label)` пишет
`logger.warning("frontmatter schema incomplete: missing …")` и **возвращает управление без
влияния на вердикт** (чистый no-op для `tuple[bool,str]`). Это удовлетворяет «по умолчанию
warning/лог» (FR-2), оставаясь поведенчески инертным.
- **Strict (`True`):** зарезервированный режим будущего ужесточения (ORCH-52d+). Когда
включён, тот же хелпер может вернуть гейту вето. На ORCH-52c флаг **остаётся `False`** в
проде и в `.env.staging` — иначе задача self-block'нется (её доки без полной схемы). Strict
покрывается unit-тестом, но не включается.
Решение «валидатор вне вердикт-пути по умолчанию» — осознанный выбор в пользу безопасности
self-hosting: машинная проверка схемы **существует и тестируется**, но **физически не может**
завалить гейт при дефолте.
### D4. Формальная спека handoff — `docs/_standards/HANDOFF_PROTOCOL.md`
Создаётся (на стадии development, как doc-deliverable) рядом с `PIPELINE_DOCS.md`. Структура
(нормативно для разработчика):
1. **Назначение + статус истины** — «источник истины поведения = код (`stages.py`,
`qg/checks.py`, `stage_engine.py`); спека документирует» (правило ORCH-075).
2. **Обязательная frontmatter-схема** — таблица 6 полей (`work_item`, `stage`, `author_agent`,
`status`, `created_at`, `model_used`) + смысл каждого; ссылка на `frontmatter.REQUIRED_FIELDS`
как на машинный источник.
3. **Контракт handoff по стадиям** — для каждой стадии (`created`→…→`done`): какие документы
**обязан** оставить выход стадии и какие frontmatter-ключи (machine-verdict ключ + будущая
общая схема). **Согласовано 1:1 с `PIPELINE_DOCS.md` §2§3** (тот же набор
документов/ключей/гейтов; различие machine-verdict vs информационные сохранено).
4. **Перекрёстная ссылка** на единый API `src/frontmatter.py` и на флаг
`frontmatter_validation_strict`.
`PIPELINE_DOCS.md` обновляется: блок «слой 1 описательный → ORCH-52c реализовала машинный
контракт» + ссылка на `HANDOFF_PROTOCOL.md` и на `src/frontmatter.py` (закрывает явную метку
«машинная проверка — отдельная задача ORCH-52c» в §5).
### D5. Без изменений API/БД/состава гейтов
Подтверждено INV-1/INV-5: HTTP-эндпоинты, `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД —
не трогаются. Опциональный счётчик валидации в `GET /queue`**не вводим** (TRZ §4: не
требование; добавил бы поверхность без нужды).
---
## Альтернативы (отклонены)
- **A1. Общий «умный» verdict-резолвер** (одна функция читает поле+токены для всех 5 гейтов).
Отклонено: token-наборы и правила различаются (особенно ORCH-047 3-поля + приоритет
негатива); единая абстракция повысила бы риск тонкого регресса на гейте → недопустимо при
self-hosting. Унифицируем только парс YAML (D2).
- **A2. Класс `Frontmatter`/новый пакет.** Отклонено: состояния нет, операции чистые; класс —
лишняя церемония и больший blast radius. Функции в существующем leaf-модуле проще и
безопаснее.
- **A3. Валидатор как hard-fail на гейте по умолчанию.** Отклонено прямо BRD/NFR-3: заблокирует
собственный деплой ORCH-52c. Default — warning-only, hard-fail под флагом (D3).
- **A4. Сторонняя библиотека `python-frontmatter`.** Отклонено: новая зависимость ради ~30
строк; `pyyaml` уже в проекте, формат тривиален, контроль над never-raise важнее.
- **A5. Ретро-фит схемы в существующие доки / правка промптов агентов.** Вне scope (это
ORCH-52d, слой 3). Схема аддитивна и forward-looking.
---
## Последствия
**Плюсы**
- Единственная точка YAML-парсинга → конец рассинхрона обработки ошибок между гейтами.
- Writer + валидатор + полная схема готовы к ORCH-52d (агенты начнут эмитить схему).
- Спека handoff закрывает пробел «что стадия обязана оставить», согласована с манифестом.
- Нулевая поведенческая регрессия по построению: семантика и reason-строки 1:1, валидатор вне
вердикт-пути при дефолте, never-raise сохранён.
**Минусы / ограничения**
- Унификация частичная (только парс, не семантика) — token-логика всё ещё живёт в каждом
гейте. Это сознательный компромисс безопасности; полная унификация семантики — возможная
будущая задача с отдельным риск-бюджетом.
- Strict-режим валидатора пока «спящий» (тестируется, но не включён) — реальная польза от
enforcement появится только с ORCH-52d.
- Reason-строки нужно перенести **дословно** — за этим следит reviewer и анти-регресс-тесты.
**Обратимость**
- `frontmatter_validation_strict=False` (дефолт) ⇒ поведение эквивалентно прежнему.
- Перевод гейтов на `parse_frontmatter` поведенчески инвариантен; откат — точечный возврат
inline-блока (но не требуется при зелёном регрессе).
**Тестирование (обязательно перед мержем)**
- `tests/test_frontmatter.py` (новый): reader (контракт неизменен), writer (round-trip
`render → parse`), валидатор (полный/неполный набор, strict on/off), битый ввод → never-raise.
- Анти-регресс на каждый из 5 гейтов: старый док-вердикт **без** новой схемы → тот же
`tuple[bool,str]`, что до задачи (NFR-1/AC-4); negative-token-приоритет tester'а (ORCH-047).
- Полный `pytest tests/ -q` зелёный.
## Связи
- Реализует: BR-1…BR-5, FR-1…FR-6, AC-1…AC-7.
- Опирается на: ORCH-075/52b (`PIPELINE_DOCS.md`, манифест), ORCH-016 (`frontmatter.py` reader),
ORCH-047 (3-полевой tester-вердикт), ORCH-022 (security-гейт), ORCH-089 (`autoDeploy`).
- Готовит почву: ORCH-52d (агенты эмитят полную схему; возможное включение strict).
- Сквозной ADR: `docs/architecture/adr/adr-0020-frontmatter-contract.md`.
- Риски/инфра/данные: `10-tech-risks.md`, `07-infra-requirements.md`, `08-data-requirements.md`.

View File

@@ -0,0 +1,50 @@
# 07 — Требования к инфраструктуре: ORCH-076 (ORCH-52c)
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
## Сводка
ORCH-52c — чисто кодово-документная задача (frontmatter-контракт + спека handoff). **Топология
инфраструктуры не меняется**: ни контейнеров, ни портов, ни volume, ни сети, ни CI-workflow.
Деплой — штатным путём конвейера через staging (8501) → прод (8500). Раздел существует для
фиксации двух операционных предусловий и одного конфиг-флага.
## Изменения инфраструктуры
- **Нет.** Compose-сервисы, порты (8500/8501), volume (`./data`, `./data/staging`), Gitea
Actions — без изменений.
- БД/миграции — нет (см. `08-data-requirements.md`).
- HTTP API — нет новых/изменённых эндпоинтов.
## Конфигурация (env)
| Ключ | Значение по умолчанию | Где | Назначение |
|------|----------------------|-----|------------|
| `ORCH_FRONTMATTER_VALIDATION_STRICT` | `false` | `.env` / `.env.staging` | Kill-switch строгой валидации схемы frontmatter. **На ORCH-52c держать `false`** (иначе self-block: доки ещё без полной схемы). Включается не раньше ORCH-52d. |
> Флаг **аддитивный**; его отсутствие в окружении эквивалентно `false` (pydantic-дефолт
> `frontmatter_validation_strict: bool = False`). Явная установка не требуется на этой задаче;
> строка в `.env.example` добавляется документации ради.
## Операционные предусловия
### П-1. Лейбл `autoDeploy` (первый боевой автодеплой — ORCH-089)
На задаче выставлен лейбл `autoDeploy`: после зелёного staging и всех тех-гейтов орк **сам**
подтверждает прод-деплой (Фаза B ORCH-036/059), без ручного «Confirm Deploy».
- Предусловие: лейбл `autoDeploy` существует в Plane-проекте ORCH и проставлен на ORCH-076
(инфра-предусловие ORCH-089). Его отсутствие = fail-safe → ручной гейт (деплой не сорвётся,
просто потребует ручного «Confirm Deploy»).
- BRD-гейт остаётся **ручным** (Слава подтверждает BRD) — `autoApprove` НЕ выставлен.
- Наблюдение: стадия `deploy` орка должна пройти через зелёные под-гейты ребра
`deploy-staging → deploy` (security → merge-gate → image-freshness → staging) до Фазы B —
`autoDeploy` физически не деплоит сломанное (BR-5 ORCH-089). Детали реакции на сбой —
`10-tech-risks.md` (R-3).
### П-2. Self-hosting рестарт-дисциплина
Прод-контейнер `orchestrator` (8500) — общий для всех проектов. Деплой ORCH-52c проходит через
штатный detached host-хук (ORCH-036), **не** ручным `docker compose`. Ручной рестарт прод-
контейнера в рамках задачи **запрещён** (встанет конвейер enduro). Откат — `orchestrator-deploy-hook.sh --rollback` (стандартный путь), не предмет этой задачи.
## Вне инфра-объёма
- Изменения промптов агентов, ретро-фит схемы в старые доки — ORCH-52d.
- Любые новые сервисы/демоны/cron — не вводятся.

View File

@@ -0,0 +1,34 @@
# 08 — Требования к данным / схеме БД: ORCH-076 (ORCH-52c)
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
## Сводка
**Изменений схемы БД нет.** Контракт frontmatter работает исключительно на **файлах**
(YAML-frontmatter номерных документов `docs/work-items/<id>/*.md`) и **in-memory** строках.
SQLite (`src/db.py`) — таблицы, индексы, миграции — **не затрагиваются** (TRZ §5).
## Детали
| Аспект | Состояние |
|--------|-----------|
| Новые таблицы | нет |
| Изменённые таблицы / колонки | нет |
| Индексы | нет |
| Миграции | нет (restart-safe без миграции) |
| Persistent state | нет (writer пишет в файлы доков, не в БД) |
## Модель данных контракта (файлы, не БД)
- **Обязательная frontmatter-схема** (машинный источник — `frontmatter.REQUIRED_FIELDS`):
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`. Это контракт
**документа**, не строки БД. Фактическое проставление полей агентами — ORCH-52d (вне scope).
- **Вердикт-ключи** (читаются единым API, семантика 1:1): `verdict:` (12), `result:`/`verdict:`/
`status:` (13, ORCH-047), `deploy_status:` (14), `staging_status:` (15), `security_status:`
(17), `post_deploy_status:` (16, информационный). Формат — ведущий YAML-блок `---…---`.
## Совместимость данных
- Старые документы-вердикты **без** новой схемы остаются валидными (схема аддитивна; её
отсутствие не влияет на чтение вердикта — NFR-1).
- Формат writer'а (`render_frontmatter`) совместим с существующим
`split("---", 2)` + `yaml.safe_load` — старые и новые парсеры читают единообразно.

View File

@@ -0,0 +1,25 @@
# 10 — Технические риски: ORCH-076 (ORCH-52c)
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
Информационный документ (гейтом не парсится). Источник истины по решениям — `06-adr/ADR-001`.
| ID | Риск | Вероятн. | Влияние | Митигация | Остаточно |
|----|------|----------|---------|-----------|-----------|
| **R-1** | **Регресс чтения вердикта на гейте** (review/testing/staging/deploy/security) при переводе на единый `parse_frontmatter` → ложный откат/застревание → **остановка конвейера ВСЕХ проектов** (главный self-hosting риск). | средняя | критическое | Унифицируется только парс YAML, НЕ семантика (ADR D2); сигнатуры/`tuple[bool,str]`/токены/upper-case/fallback `worktree→origin/main` 1:1; reason-строки переносятся дословно через `FrontmatterParse`-состояния; **анти-регресс-тест на каждый из 5 гейтов** (старый док → тот же вердикт) + полный `pytest tests/` зелёный до мержа (AC-4/AC-6). | низкое |
| **R-2** | **Самоблокировка валидатором** на собственном деплое: доки ORCH-52c (и соседей) ещё без полной 6-польной схемы → strict-валидатор завалил бы гейт. | высокая (если включить strict) | высокое | `frontmatter_validation_strict` дефолт `False`; валидатор в default-режиме **вне вердикт-пути** гейтов (warning-only, чистый no-op для boolean); strict тестируется, но НЕ включается до ORCH-52d; `false` в `.env`/`.env.staging` (NFR-3, ADR D3). | очень низкое |
| **R-3** | **Первый боевой `autoDeploy`** (ORCH-089): орк сам подтверждает прод-рестарт после staging → если регресс R-1 проскользнул мимо тестов, автодеплой выкатит его без человеческой паузы. | низкая | высокое | `autoDeploy` достигает Фазы B только после зелёных под-гейтов ребра `deploy-staging→deploy` (security→merge-gate→image-freshness→staging) — не деплоит сломанное (BR-5 ORCH-089); обязательная страховка staging (8501); пост-деплой мониторинг ORCH-021 (`ALERT_ONLY` для self) ловит «зелёный деплой, красный прод» с откатом durable-freeze (ORCH-088); ручной BRD-гейт сохранён. Наблюдать стадию `deploy` вживую (Telegram-карточка). | низкое |
| **R-4** | **Дрейф reason-строк / сообщений гейтов** при рефакторе → тесты, ассертящие текст, краснеют; логи/Plane-комменты меняют формулировку. | средняя | низкое | Маппинг `FrontmatterParse → прежняя reason-строка` зафиксирован в ADR D2; переносить дословно; ассерты в анти-регресс-тестах фиксируют текущий текст. | низкое |
| **R-5** | **Расхождение спеки handoff с фактом кода** → «лживый» стандарт. | средняя | среднее | `HANDOFF_PROTOCOL.md` согласован 1:1 с `PIPELINE_DOCS.md` §2§3 (тот же набор документов/ключей/гейтов); явная пометка «источник истины — код» (правило ORCH-075); reviewer сверяет (CLAUDE.md №2/№6). | низкое |
| **R-6** | **Скрытое исключение из writer/валидатора** прорывается в конвейер (нарушение never-raise) на битом вводе. | низкая | высокое | Контракт всего модуля never-raise (как действующий reader): любая ошибка → лог + безопасное значение; **тест на битом вводе** (невалидный YAML, не-mapping, I/O-ошибка) подтверждает отсутствие проброса (AC-5/NFR-2). | очень низкое |
| **R-7** | **Циклический импорт** при использовании `frontmatter` из `qg/checks.py`/`security_gate.py`/`post_deploy.py`/`review_parse.py`. | низкая | среднее | `frontmatter.py` — leaf без проектных зависимостей (только `logging` + ленивый `yaml`); импортируется, не импортирует проектные модули — циклов нет. | очень низкое |
| **R-8** | **Частичная унификация** (token-логика осталась в каждом гейте) воспринимается reviewer'ом как недовыполнение AC-3. | низкая | низкое | AC-3 требует «парсить YAML через единый API, а не дублированной логикой» — выполнено (D2); неунификация семантики — осознанный выбор безопасности, зафиксирован в ADR (альтернатива A1 отклонена). | очень низкое |
## Сводные митигации (обязательные перед мержем)
1. Анти-регресс-тест на каждый из 5 вердикт-гейтов (старый док без схемы → прежний вердикт).
2. `tests/test_frontmatter.py`: reader (контракт неизменен) / writer (round-trip) / валидатор
(полный/неполный, strict on/off) / битый ввод → never-raise.
3. Полный `pytest tests/ -q` зелёный.
4. `frontmatter_validation_strict=False` в прод/staging env.
5. Живое наблюдение стадии `deploy` (первый `autoDeploy`); готовность к
`orchestrator-deploy-hook.sh --rollback` штатным путём (не ручной рестарт).

View File

@@ -0,0 +1,93 @@
---
type: review
work_item_id: ORCH-076
verdict: APPROVED
version: 1
---
# Review ORCH-076 — ORCH-52c: единый frontmatter-контракт + спека handoff
## Summary
Изменение реализует слой 2 эпика ORCH-52: `src/frontmatter.py` превращён из single-key
reader'а в полный машинный контракт (reader + writer + валидатор схемы + единый парс-примитив),
а дублированное чтение YAML-frontmatter в пяти вердикт-парсерах сведено к одной точке
(`parse_frontmatter`). Дополнительно создана формальная спека handoff
(`docs/_standards/HANDOFF_PROTOCOL.md`).
Реализация **полностью соответствует ТЗ и ADR-001/adr-0020**: унифицирован только МЕХАНИЗМ
парсинга (D2), семантика вердиктов, token-логика, приоритет негативного токена, fallback
`worktree→origin/main` и трёх-полевой контракт tester (ORCH-047) сохранены 1:1. Валидатор
warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`
(дефолт `False`) — критично для self-hosting (задача не self-block'ится). Весь модуль
never-raise. `STAGE_TRANSITIONS` и состав `QG_CHECKS` не тронуты (подтверждено TC-16).
Проверка по осям:
- **Соответствие ТЗ:** FR-1…FR-6 реализованы (writer, валидатор, полночтение, единый контракт
вердиктов, BC старых доков, спека handoff). API/БД не тронуты (TRZ §4§5). ✓
- **Соответствие AC:** AC-1…AC-7 выполнены (см. ниже). ✓
- **Соответствие ADR:** D1D5 реализованы как спроектировано (функции в leaf-модуле,
unify-механизм-не-семантику, warning-only validator, спека handoff, без API/БД). ✓
- **Качество кода:** docstrings на всех публичных функциях; never-raise контракт выдержан
(broad-except + лог + безопасный возврат); leaf-модуль без проектных импортов (cycle-free).
- **Тесты:** содержательные, покрывают writer/round-trip/валидатор/strict/never-raise/reader
(`test_frontmatter.py`), семантику пяти гейтов + BC + origin/main fallback
(`test_qg_verdicts.py`), security-гейт (`test_security_gate.py`), инварианты реестра
(`test_stages_invariants.py`). Полный регресс **1212 passed**.
Проверка AC:
- **AC-1** (reader+writer+валидатор, unit-tested): ✓ `read_frontmatter_value` (BC),
`render/write_frontmatter`, `validate_schema` с 6 полями `REQUIRED_FIELDS`; TC-01…TC-07.
- **AC-2** (спека handoff в `docs/_standards/`, согласована): ✓ покрывает все стадии
`created``done`, набор документов/ключей/гейтов 1:1 с `PIPELINE_DOCS.md` §2§3;
`PIPELINE_DOCS.md` обновлён ссылкой + отметкой реализации (§5§6).
- **AC-3** (единый контракт вердиктов): ✓ все 5 (`check_reviewer_verdict`,
`_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`,
`parse_security_status`) делегируют `parse_frontmatter`; ad-hoc блоки удалены.
- **AC-4** (BC старых доков + регресс зелёный): ✓ TC-13/TC-14 для старых доков без схемы;
1212 tests green.
- **AC-5** (never-raise + warning-only + kill-switch): ✓ TC-05 (битый ввод), `maybe_warn_schema`
инертен при дефолте, `frontmatter_validation_strict` в `config.py`.
- **AC-6** (`STAGE_TRANSITIONS`/`QG_CHECKS` неизменны, семантика 1:1): ✓ TC-16; вердикт-логика
не тронута.
- **AC-7** (документация): ✓ см. раздел «Документация».
## Findings
### P0 — Blocker
- Нет.
### P1 — Must fix
- Нет.
### P2 — Should fix
- Нет.
### P3 — Nice-to-have
- [ ] `_parse_tests_verdict` (`src/qg/checks.py`): для редкого случая «валидный YAML, но не
mapping» во frontmatter старая ветка возвращала reason `"Malformed YAML frontmatter in test
report (not a mapping)"`, новая реализация маршрутизирует этот ввод в путь пустых данных →
reason `"No machine-readable verdict/status/result in test report frontmatter"`.
**Boolean-вердикт идентичен (`False` в обоих случаях) → семантика и STAGE_TRANSITIONS не
затронуты (AC-6 соблюдён).** Расхождение только в reason-строке (лог/коммент). ADR D2 заявляет
«reason-строки 1:1» — здесь незначительное отклонение в крайне редком кейсе. Можно при желании
добавить явную ветку для паритета, но это не обязательно.
- [ ] `parse_security_status` (`src/security_gate.py`) не вызывает `maybe_warn_schema`, тогда как
4 из 5 вердикт-парсеров его вызывают. Поскольку warning инертен (не влияет на вердикт), это
чисто косметическая несогласованность наблюдаемости. Для единообразия можно добавить вызов.
## Документация
Обновлено в том же PR (golden source синхронен с кодом, правило CLAUDE.md №2/№6):
- `CLAUDE.md` — блок про единый frontmatter-контракт в «Конвенциях».
- `docs/architecture/README.md` — «Канон гейтов» (единый контракт), компонент frontmatter,
ссылки на спеку handoff и adr-0020.
- `docs/_standards/HANDOFF_PROTOCOL.md`**создан** (спека handoff, все стадии, обязательная
схема `REQUIRED_FIELDS`).
- `docs/_standards/PIPELINE_DOCS.md` — обновлён (слой 2 реализован, §5§6 + ссылки).
- `CHANGELOG.md` — детальная запись `[Unreleased]`.
- ADR: per-work-item `docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md` +
сквозной `docs/architecture/adr/adr-0020-frontmatter-contract.md`; индекс
`docs/architecture/adr/README.md` обновлён (adr-0018/0019/0020, max=0020).
Документация полная и согласованная — претензий нет.

View File

@@ -0,0 +1,85 @@
---
type: test-report
work_item_id: ORCH-076
result: PASS
---
# Test Report — ORCH-076
ORCH-52c: протокол handoff + единый frontmatter-контракт (writer/валидатор/схема).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: `feature/ORCH-076-orch-52c-handoff-frontmatter-w`
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-076-orch-52c-handoff-frontmatter-w`
- Дата: 2026-06-09
## Предусловия
- Review-вердикт `12-review.md`: **APPROVED** (P0/P1/P2 — нет; два P3 nice-to-have, не блокирующие).
- Prod health (8500): `{"status":"ok","service":"orchestrator"}` — конвейер прочих проектов не тронут.
## Smoke test API (prod 8500, read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
| `GET /status` | PASS — задача ORCH-076 видна на стадии `testing` (id 69) |
| `GET /queue` | PASS — counts/reconcile/reaper/post_deploy/merge_verify в норме (done=897, failed=4) |
## Результаты по тест-плану (04-test-plan.yaml)
| TC ID | Описание | Модуль | Результат |
|-------|----------|--------|-----------|
| TC-01 | Writer сериализует mapping в каноничный YAML-frontmatter | tests/test_frontmatter.py | PASS |
| TC-02 | Round-trip writer → read_frontmatter_value | tests/test_frontmatter.py | PASS |
| TC-03 | Валидатор: полная схема → valid=True | tests/test_frontmatter.py | PASS |
| TC-04 | Валидатор: неполная схема → valid=False, без исключения | tests/test_frontmatter.py | PASS |
| TC-05 | never-raise: writer/валидатор на битом вводе | tests/test_frontmatter.py | PASS |
| TC-06 | reader read_frontmatter_value: прежний контракт (BC) | tests/test_frontmatter.py | PASS |
| TC-07 | kill-switch frontmatter_validation_strict (False/True) | tests/test_frontmatter.py | PASS |
| TC-08 | check_reviewer_verdict через единый API (APPROVED/REQUEST_CHANGES/missing) | tests/test_qg_verdicts.py | PASS |
| TC-09 | _parse_tests_verdict: ORCH-047 3 поля + приоритет негативного токена | tests/test_qg_verdicts.py | PASS |
| TC-10 | _parse_deploy_status: SUCCESS/FAILED/missing (БАГ-8 1:1) | tests/test_qg_verdicts.py | PASS |
| TC-11 | _parse_staging_status: SUCCESS/FAILED + условность ORCH-35 | tests/test_qg_verdicts.py | PASS |
| TC-12 | parse_security_status: PASS/FAIL семантика 1:1 | tests/test_security_gate.py | PASS |
| TC-13 | Старый док-вердикт без новой схемы читается всеми 5 гейтами | tests/test_qg_verdicts.py | PASS |
| TC-14 | Док с полной схемой + вердикт-ключом — тот же вердикт (схема аддитивна) | tests/test_qg_verdicts.py | PASS |
| TC-15 | fallback worktree → origin/main сохранён через единый API | tests/test_qg_verdicts.py | PASS |
| TC-16 | Состав QG_CHECKS и STAGE_TRANSITIONS не изменён | tests/test_stages_invariants.py | PASS |
| TC-17 | Полный прогон tests/ зелёный (анти-регресс) | tests/ | PASS |
Все 17 тест-кейсов плана покрыты и зелёные. TC-таргетные модули
(`test_frontmatter.py`, `test_qg_verdicts.py`, `test_security_gate.py`,
`test_stages_invariants.py`) — **49 passed**.
## Покрытие критериев приёмки (03-acceptance-criteria.md)
| AC | Подтверждено | Результат |
|----|--------------|-----------|
| AC-1 reader+writer+валидатор, unit-tested | TC-01…TC-07 | PASS |
| AC-2 спека handoff в docs/_standards/ согласована | (review/doc-check) | PASS |
| AC-3 единый контракт вердиктов (5 точек) | TC-08…TC-12 | PASS |
| AC-4 BC старых доков + регресс зелёный + самопрохождение | TC-13/TC-14 + полный регресс + задача на testing | PASS |
| AC-5 never-raise + warning-only + kill-switch | TC-05/TC-07 | PASS |
| AC-6 STAGE_TRANSITIONS/QG_CHECKS неизменны | TC-16 | PASS |
| AC-7 документация обновлена | review «Документация» | PASS |
## Вывод pytest
Полный регресс:
```
1212 passed, 1 warning in 34.97s
```
TC-таргетные модули:
```
49 passed, 1 warning in 0.44s
```
Единственное предупреждение — `PydanticDeprecatedSince20` (class-based config в
`src/config.py`), предсуществующее, не связано с ORCH-076, не влияет на результат.
Сетевых зависимостей в тестах нет (frontmatter — файловый/in-memory контракт).
## Итог
**PASS** — полный регресс зелёный (1212 passed), все 17 TC плана PASS, smoke API OK,
prod-контейнер не тронут. Регрессий гейтов (review/tester/deploy/staging/security) нет,
семантика вердиктов 1:1. Задача готова к переходу на `deploy-staging`.

View File

@@ -0,0 +1,42 @@
---
staging_status: SUCCESS
timestamp: 2026-06-09T11:13:04Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (8501).
Run canonically **inside** the `orchestrator-staging` container (ORCH-048 / ADR-001) via the
Docker Engine API `exec` (equivalent to `docker exec`; the host `docker` CLI was unavailable,
but the script still executed in-container so B6 reads the instance's own `.env.staging`
process-env — no false registry FAIL).
Command:
```
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
## Notes
- **Exit code: 0** → `staging_status: SUCCESS` (trusting the exit code per ORCH-061; waived
checks are not re-judged).
- All REAL pipeline checks passed: A1/A2/A3 (smoke), B4/B5/B6 (access + registry isolation),
C7/C8 (E2E issue create + webhook trigger).
- The only failures are the two SANDBOX_INFRA checks **C9a/C9b**, which depend on SANDBOX bot
accounts being members of the sandbox Plane project — an infra precondition, not a pipeline
regression. They are tolerated (`ORCH_STAGING_INFRA_TOLERANCE_ENABLED=true`) while every REAL
check is green; the script printed the `INFRA-WAIVED:` / `VERDICT:` lines above and exited 0.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-52g: убрать Telegram link-preview (логотип Plane) в уведомлениях трекера
Work Item ID: ORCH-080
## Description
TBD

View File

@@ -0,0 +1,73 @@
# 01-BRD — ORCH-080: убрать Telegram link-preview (логотип Plane) в уведомлениях трекера
Work Item ID: ORCH-080
Эпик: ORCH-052 (под-задача ORCH-52g)
Тип: Доработка (UX уведомлений)
Приоритет: LOW (косметика)
Зона: `src/notifications.py`
## 1. Контекст и проблема
Каждая задача в Telegram сопровождается одной live-карточкой трекера (`src/notifications.py`,
ORCH-042/066/067). С ORCH-067 в карточке появился **кликабельный номер задачи**
`<a href="https://plane.mva154.duckdns.org/.../issues/<id>/">ORCH-NNN</a>`.
Telegram по умолчанию разворачивает **link-preview** (web page preview) для первой ссылки
в сообщении. Из-за ссылки на Plane под каждым сообщением трекера раскрывается крупный
баннер-превью **«Plane — Modern project management»**.
**Жалоба (Слава, 08.06):** баннер уродует ленту чата и дублируется на каждой задаче/каждом
обновлении карточки (особенно заметно в дефолтном режиме `bump`, где карточка пересоздаётся
на каждом переходе).
## 2. Диагностика (код-аудит `src/notifications.py`)
| Функция | Эндпоинт | Текущий JSON-payload | Превью |
|---------|----------|----------------------|--------|
| `send_telegram()` (стр. 52-62) | `POST /sendMessage` | `chat_id`, `text`, `parse_mode: HTML`, `disable_notification` | **разворачивается** (нет `disable_web_page_preview`) |
| `edit_telegram()` (стр. 165-174) | `POST /editMessageText` | `chat_id`, `message_id`, `text`, `parse_mode: HTML` | **разворачивается** (нет `disable_web_page_preview`) |
Причина баннера: оба payload **не содержат** ключ `disable_web_page_preview`. Telegram Bot API
по умолчанию (отсутствие ключа) включает превью.
`delete_telegram()` (`/deleteMessage`) превью не порождает — правки не требует.
## 3. Бизнес-цель
Карточка трекера и уведомления в Telegram **не должны** показывать баннер link-preview Plane,
при этом ссылка на задачу **остаётся кликабельной**.
## 4. Бизнес-требования
- **BR-1.** В payload `sendMessage` (`send_telegram`) присутствует `disable_web_page_preview: True`.
- **BR-2.** В payload `editMessageText` (`edit_telegram`) присутствует `disable_web_page_preview: True`.
- **BR-3.** Баннер-превью Plane больше не появляется ни под карточкой трекера (оба режима
`bump`/`edit`), ни под отдельными notify-сообщениями, которые идут через `send_telegram`
(`notify_approve_requested`, `notify_error`, alert'ы стадий) — все они используют тот же
низкоуровневый примитив.
- **BR-4.** Кликабельная ссылка `<a href>` на задачу в Plane сохраняется (`parse_mode: HTML`
не меняется).
- **BR-5.** Контракт **never-raise** сохранён: отправка/редактирование никогда не валит
оркестратор; `pytest` зелёный.
## 5. Не-цели (вне скоупа)
- Не менять текст/формат/верстку карточки.
- Не трогать `parse_mode` (HTML нужен для `<a href>`).
- Не трогать bump/edit-логику (`update_task_tracker`), репойнт `tracker_message_id`,
delete-семантику.
- Не вводить флаги/конфиг — поведение «без превью» безусловное (превью никому не нужно).
- Не трогать схему БД.
## 6. Заинтересованные лица
- **Слава (Owner)** — инициатор, конечный наблюдатель ленты Telegram.
## 7. Грабли / координация
- Файл `src/notifications.py` затрагивает также ORCH-067 (и потенциально другие задачи эпика).
Сверить, что правки (две строки) не конфликтуют при merge.
- Один репозиторий с ORCH-74 → по ORCH-026 действует сериализация merge.
Запускать **после** того как ORCH-74 доедет в `main` (или когда конвейер свободен),
чтобы не плодить параллельный merge в `orchestrator`.
- Деплой — штатный через **Confirm Deploy** (self-hosting, ORCH-059).

View File

@@ -0,0 +1,102 @@
# 02-TRZ — ORCH-080: убрать Telegram link-preview в уведомлениях трекера
Work Item ID: ORCH-080
Зона изменений: `src/notifications.py` (две строки)
## 1. Задействованные модули `src/`
- `src/notifications.py`**единственный** изменяемый модуль:
- `send_telegram(text, disable_notification=False)` — обёртка `POST .../sendMessage`.
- `edit_telegram(message_id, text)` — обёртка `POST .../editMessageText`.
Косвенно затронуты (поведение улучшается без изменения их кода — они вызывают изменённые
примитивы): `update_task_tracker` (bump+edit), `notify_approve_requested`, `notify_error`,
а также вызовы `send_telegram` из `launcher`/`stage_engine` (alert'ы деплоя/падений).
## 2. Изменения кода
### 2.1. `send_telegram()` — добавить ключ в JSON-payload `httpx.post`
В словаре `json={...}` вызова `sendMessage` (текущие стр. 55-60) добавить строку:
```python
"disable_web_page_preview": True,
```
Итоговый payload:
```python
json={
"chat_id": s.telegram_chat_id,
"text": text,
"parse_mode": "HTML",
"disable_notification": disable_notification,
"disable_web_page_preview": True,
},
```
### 2.2. `edit_telegram()` — добавить ключ в JSON-payload `httpx.post`
В словаре `json={...}` вызова `editMessageText` (текущие стр. 168-173) добавить строку:
```python
"disable_web_page_preview": True,
```
Итоговый payload:
```python
json={
"chat_id": s.telegram_chat_id,
"message_id": message_id,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": True,
},
```
> Примечание: Telegram Bot API исторически принимает top-level `disable_web_page_preview`
> для `sendMessage`/`editMessageText` (актуальная схема также поддерживает
> `link_preview_options.is_disabled`, но top-level флаг остаётся валиден и совместим).
> Используем top-level флаг — минимальная, обратносовместимая правка, как указано в задаче.
## 3. Изменения API
Нет изменений внутреннего HTTP API оркестратора. Меняется только тело исходящих запросов к
Telegram Bot API (добавлен один булев ключ в payload двух методов).
## 4. Изменения схемы БД
Нет.
## 5. Требования к новым QG checks
Нет. Новые Quality Gate проверки не вводятся.
## 6. Конфиг / флаги
Нет. Поведение «без превью» — безусловное (kill-switch не требуется: превью трекера
не нужно никому, риск регрессии нулевой; правка обратимая одной строкой).
`parse_mode`, `disable_notification`, bump/edit-логика — без изменений.
## 7. Артефакты, обновляемые по pipeline
- `CHANGELOG.md` — запись в `## [Unreleased]` (тип `fix:` — косметика UX уведомлений).
- Документация: правка `src/notifications.py` затрагивает поведение, описанное в
`CLAUDE.md` (раздел «Нотификации / Telegram live-tracker») и
`docs/architecture/README.md` (компонент Notifications). Достаточно короткой ремарки,
что карточка/уведомления шлются без web-page-preview (по желанию архитектора — определить
объём в ADR; ADR не обязателен для столь малой косметики, решение за архитектором).
## 8. Контракты-инварианты (не нарушать)
- **never-raise**: обе функции по-прежнему ловят все исключения (`try/except: pass`/`return`)
и не валят оркестратор.
- Возвращаемые значения не меняются: `send_telegram``message_id|None`,
`edit_telegram``EDIT_*`.
- `parse_mode: "HTML"` сохранён в обоих payload (иначе `<a href>` сломается).
- `disable_notification` в `send_telegram` сохранён (карточка тихая).
- Инвариант «одна карточка на задачу» (bump/edit) не затрагивается.
## 9. Commit / ветка
- Ветка: `feature/ORCH-080-orch-52g-telegram-link-preview` (существует).
- Commit: `fix: disable Telegram link-preview in tracker notifications (ORCH-080)`.

View File

@@ -0,0 +1,59 @@
# 03-Acceptance Criteria — ORCH-080
Work Item ID: ORCH-080
Каждый критерий имеет явное условие PASS/FAIL.
## AC-1 — `disable_web_page_preview` в payload `sendMessage`
- **PASS:** JSON-payload вызова `httpx.post(.../sendMessage)` в `send_telegram()` содержит
ключ `"disable_web_page_preview"` со значением `True`.
- **FAIL:** ключ отсутствует или `False`.
- **Проверка:** unit-тест (мок `httpx`) инспектирует `httpx.post.call_args.kwargs["json"]`.
## AC-2 — `disable_web_page_preview` в payload `editMessageText`
- **PASS:** JSON-payload вызова `httpx.post(.../editMessageText)` в `edit_telegram()` содержит
ключ `"disable_web_page_preview"` со значением `True`.
- **FAIL:** ключ отсутствует или `False`.
- **Проверка:** unit-тест (мок `httpx`) инспектирует `httpx.post.call_args.kwargs["json"]`.
## AC-3 — баннер link-preview Plane исчез в карточке трекера
- **PASS:** в реальном чате Telegram карточка трекера задачи (режимы `bump` и `edit`)
больше не показывает баннер «Plane — Modern project management».
- **FAIL:** баннер всё ещё разворачивается.
- **Проверка:** ручная верификация на staging (8501) после деплоя — наблюдение карточки в
Telegram. Автоматически косвенно покрыто AC-1/AC-2 (payload содержит флаг).
## AC-4 — ссылка на задачу остаётся кликабельной
- **PASS:** в карточке/уведомлениях номер задачи `ORCH-NNN` остаётся кликабельной ссылкой
`<a href=...>` на issue в Plane; `parse_mode: "HTML"` сохранён в обоих payload.
- **FAIL:** `parse_mode` изменён/удалён, либо ссылка перестала рендериться как `<a href>`.
- **Проверка:** unit-тест проверяет, что `"parse_mode": "HTML"` присутствует в обоих payload;
существующие тесты ссылок (`test_notify_issue_links.py`) остаются зелёными.
## AC-5 — сохранены существующие поля payload
- **PASS:** `send_telegram` payload по-прежнему содержит `chat_id`, `text`, `parse_mode`,
`disable_notification`; `edit_telegram` payload — `chat_id`, `message_id`, `text`,
`parse_mode`. Возвращаемые значения функций не изменились
(`send_telegram → message_id|None`, `edit_telegram → EDIT_*`).
- **FAIL:** любое из перечисленных полей удалено/переименовано, либо изменился контракт
возврата.
- **Проверка:** unit-тесты payload + существующие тесты трекера/классификации исходов.
## AC-6 — never-raise сохранён, pytest зелёный
- **PASS:** при сетевой/HTTP-ошибке `send_telegram`/`edit_telegram` не бросают исключение
(возврат `None`/`EDIT_FAILED`); вся сюита `pytest tests/ -q` зелёная.
- **FAIL:** любое исключение наружу или красный pytest.
- **Проверка:** существующие тесты never-raise (`test_resilience.py`,
`test_telegram_tracker.py`) + полный прогон.
## AC-7 — документация обновлена в том же PR
- **PASS:** `CHANGELOG.md` содержит запись об ORCH-080; при необходимости — короткая ремарка
в `CLAUDE.md`/`docs/architecture/README.md` о подавлении link-preview.
- **FAIL:** функционал изменён, документация не обновлена (Reviewer → REQUEST_CHANGES).

View File

@@ -0,0 +1,76 @@
work_item: ORCH-080
description: >
Подавление Telegram link-preview (disable_web_page_preview: True) в payload
send_telegram (sendMessage) и edit_telegram (editMessageText). Сохранить
parse_mode HTML, disable_notification, never-raise и контракты возврата.
tests:
- id: TC-01
type: unit
description: >
send_telegram() кладёт "disable_web_page_preview": True в JSON-payload
httpx.post(.../sendMessage). Проверка через мок httpx и инспекцию
httpx.post.call_args.kwargs["json"].
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-02
type: unit
description: >
edit_telegram() кладёт "disable_web_page_preview": True в JSON-payload
httpx.post(.../editMessageText). Проверка через мок httpx и инспекцию
payload.
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-03
type: unit
description: >
Регрессия parse_mode: оба payload (sendMessage и editMessageText)
по-прежнему содержат "parse_mode": "HTML" — ссылка <a href> остаётся
кликабельной (AC-4).
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-04
type: unit
description: >
Регрессия полей send_telegram: payload содержит chat_id, text,
parse_mode, disable_notification; disable_notification прокидывается
из аргумента (True/False) без изменений (AC-5).
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-05
type: unit
description: >
Контракты возврата не изменились: send_telegram возвращает message_id
при ok:true, None при отсутствии креденшелов/ошибке; edit_telegram
возвращает EDIT_OK при ok:true (AC-5, AC-6).
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-06
type: unit
description: >
never-raise: при httpx.post бросающем исключение send_telegram->None и
edit_telegram->EDIT_FAILED, без проброса исключения (AC-6).
module: tests/test_link_preview_disabled.py
expected: PASS
- id: TC-07
type: integration
description: >
Полный прогон существующей сюиты трекера/уведомлений остаётся зелёным
(нет регрессий bump/edit-логики, классификации исходов, ссылок):
pytest tests/test_telegram_tracker.py tests/test_tracker_bump.py
tests/test_notify_issue_links.py tests/test_resilience.py.
module: tests/test_telegram_tracker.py
expected: PASS
- id: TC-08
type: integration
description: >
Вся сюита pytest tests/ -q зелёная (общая регрессия, AC-6).
module: tests/
expected: PASS

View File

@@ -0,0 +1,63 @@
# ADR-001: Подавление Telegram link-preview в низкоуровневых примитивах нотификаций
## Статус
Accepted
## Контекст
С ORCH-067 карточка трекера и notify-сообщения несут кликабельный номер задачи
`<a href="https://plane.mva154.duckdns.org/.../issues/<id>/">ORCH-NNN</a>`. Telegram
Bot API по умолчанию (при отсутствии ключа `disable_web_page_preview`) разворачивает
web-page-preview для первой ссылки в сообщении — под каждым сообщением трекера
раскрывается баннер «Plane — Modern project management». В дефолтном режиме `bump`
(ORCH-067) карточка пересоздаётся на каждом переходе, поэтому баннер дублируется на
каждой задаче и каждом обновлении, засоряя ленту (жалоба Owner, 08.06).
Код-аудит (`src/notifications.py`) подтвердил причину: JSON-payload обоих
низкоуровневых примитивов — `send_telegram()` (`POST /sendMessage`, стр. 55-60) и
`edit_telegram()` (`POST /editMessageText`, стр. 168-173) — **не содержит** ключ
`disable_web_page_preview`. Все вышестоящие нотификации (`update_task_tracker` в обоих
режимах, `notify_approve_requested`, `notify_error`, alert'ы стадий из
`launcher`/`stage_engine`) проходят через эти два примитива.
## Решение
Добавить `"disable_web_page_preview": True` в JSON-payload `httpx.post` обоих примитивов:
`send_telegram()` и `edit_telegram()`. Изменение — **на уровне низкоуровневого
примитива**, а не на уровне каждого вызова, потому что:
1. **Единая точка** — все исходящие сообщения трекера/нотификаций идут через эти две
функции; правка двух строк гасит баннер у ВСЕХ потребителей (карточка `bump`/`edit`,
notify-хелперы, alert'ы) без изменения их кода.
2. **Безусловно, без флага** — превью Plane не нужно никому (это не данные, а навигация
по ссылке, которая остаётся кликабельной). Kill-switch не вводится: риск регрессии
нулевой, правка обратима одной строкой. Это согласуется с принципом «минимум
зависимостей/конфигурации».
3. **Top-level флаг, а не `link_preview_options.is_disabled`** — top-level
`disable_web_page_preview` остаётся валиден и обратносовместим в Bot API; это
минимальная правка без введения вложенной структуры.
`parse_mode: "HTML"` сохраняется в обоих payload (иначе `<a href>` перестанет
рендериться — ссылка должна остаться кликабельной). `disable_notification`,
bump/edit-логика, repoint `tracker_message_id`, delete-семантика, контракты возврата
(`send_telegram → message_id|None`, `edit_telegram → EDIT_*`) — не затрагиваются.
## Последствия
**Плюсы:**
- Баннер link-preview исчезает под карточкой трекера (оба режима) и под всеми
notify/alert-сообщениями — одна правда в двух примитивах.
- Ссылка на задачу остаётся кликабельной (HTML сохранён).
- Нулевой риск: ключ аддитивный, контракты примитивов и инвариант «одна карточка на
задачу» не меняются; `never-raise` (`try/except`) сохранён.
**Минусы / ограничения:**
- Поведение безусловное — нет конфигурации «вернуть превью». Сознательный выбор:
превью трекера не имеет ценности, флаг был бы лишней поверхностью.
**Не затрагивается:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД, `parse_mode`,
`disable_notification`, транспортные хелперы `delete_telegram`/repoint-логика. Глобальный
ADR не требуется — решение локально для `src/notifications.py`, не сквозное.
## Self-hosting
Изменение не требует немедленного рестарта прод-контейнера и не меняет топологию.
Деплой — штатный через staging (8501) → `Confirm Deploy` (ORCH-059). По ORCH-026
(сериализация merge одного репо) задача мержится после освобождения конвейера
`orchestrator` (координация с ORCH-074 — см. BRD §7).

View File

@@ -0,0 +1,22 @@
# 10-Tech Risks — ORCH-080
Work Item ID: ORCH-080
Зона: `src/notifications.py` (две строки в `send_telegram`/`edit_telegram`)
Косметическая правка UX (LOW). Топология, схема БД, стадии, QG — не меняются.
Риск регрессии оценён как **нулевой**; ниже — остаточные пункты для внимания.
| # | Риск | Вероятность | Влияние | Митигация |
|---|------|-------------|---------|-----------|
| R-1 | Опечатка ключа/значения (`disable_web_page_preview`) — баннер не гаснет | Низкая | Низкое (косметика) | unit-тест AC-1/AC-2 инспектирует `httpx.post.call_args.kwargs["json"]`; ручная верификация на staging (AC-3) |
| R-2 | Случайное удаление `parse_mode: "HTML"` → ссылка `<a href>` ломается | Очень низкая | Среднее (теряется кликабельность) | AC-4: unit-тест на наличие `parse_mode: "HTML"` в обоих payload; `test_notify_issue_links.py` остаётся зелёным |
| R-3 | Merge-конфликт с ORCH-067/ORCH-074 в `src/notifications.py` | Низкая | Низкое | По ORCH-026 сериализация merge одного репо; запуск после доезда ORCH-74 в `main` (BRD §7); pre-merge rebase (ORCH-043) |
| R-4 | Регрессия контракта возврата примитивов (`message_id|None` / `EDIT_*`) | Очень низкая | Среднее | Правка строго аддитивна (новый ключ в payload), возвраты не трогаются; AC-5 + существующие тесты трекера |
| R-5 | Telegram депрекейтит top-level `disable_web_page_preview` в пользу `link_preview_options` | Очень низкая | Низкое (forward-compat) | Top-level флаг остаётся валиден и обратносовместим; миграция на `link_preview_options.is_disabled` — отдельная задача при необходимости |
## Инварианты, которые НЕЛЬЗЯ нарушить
- `never-raise` обоих примитивов (`try/except` сохранён).
- `parse_mode: "HTML"` в обоих payload (иначе `<a href>` ломается).
- `disable_notification` в `send_telegram` (карточка тихая).
- Инвариант «одна карточка на задачу» (bump/edit) — не затрагивается.
- Контракты возврата: `send_telegram → message_id|None`, `edit_telegram → EDIT_*`.

View File

@@ -0,0 +1,72 @@
---
type: review
work_item_id: ORCH-080
verdict: APPROVED
version: 1
---
# Review ORCH-080
## Summary
Задача убирает баннер Telegram link-preview («Plane — Modern project management»),
который разворачивался под кликабельной ссылкой `ORCH-NNN` в карточке трекера и
во всех notify/alert-сообщениях. Решение точно соответствует TRZ и ADR-001:
добавлен ключ `"disable_web_page_preview": True` в JSON-payload обоих
низкоуровневых примитивов `send_telegram` (`POST /sendMessage`) и `edit_telegram`
(`POST /editMessageText`) — единая точка для всех потребителей, без kill-switch,
без изменения контрактов. Изменение минимально (2 строки + комментарии),
аддитивно и обратимо.
Проверены все четыре оси (ТЗ, ADR, качество кода, тесты) + документация. Findings
уровней P0/P1/P2 — нет.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
## Соответствие ТЗ и AC
- TRZ §2.1/§2.2 — ключ добавлен в оба payload в точности как предписано. ✅
- AC-1 — `disable_web_page_preview: True` в `sendMessage` payload (TC-01). ✅
- AC-2 — то же в `editMessageText` payload (TC-02). ✅
- AC-3 — баннер исчезает (ручная верификация на staging; косвенно покрыто AC-1/AC-2). ✅
- AC-4 — `parse_mode: "HTML"` сохранён в обоих payload, ссылка кликабельна (TC-03);
`tests/test_notify_issue_links.py` зелёный. ✅
- AC-5 — поля `chat_id/text/parse_mode/disable_notification` (send) и
`chat_id/message_id/text/parse_mode` (edit) сохранены; контракты возврата
(`message_id|None`, `EDIT_*`) не изменились (TC-04/TC-05). ✅
- AC-6 — never-raise сохранён (TC-06); полный прогон `pytest tests/ -q`**1058 passed**. ✅
- AC-7 — документация обновлена в том же PR (см. ниже). ✅
## Соответствие ADR
ADR-001 (Accepted): правка на уровне примитива (а не каждого вызова), безусловно
без флага, top-level `disable_web_page_preview` вместо `link_preview_options`,
`parse_mode: HTML` сохранён, контракты и инвариант «одна карточка на задачу» не
тронуты. Реализация соответствует решению 1:1. Глобальные ADR не нарушены
(`STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД — без изменений). ✅
## Качество кода
- Изменение минимальное, целевое; комментарии ссылаются на ORCH-080 и поясняют цель.
- `try/except` never-raise в обеих функциях не затронут; пути без кредов и контракты
возврата сохранены.
- Тесты содержательные: инспектируют реальный payload через мок `httpx`
(`call_args.kwargs["json"]`), покрывают флаг, регрессию `parse_mode`/полей,
контракты возврата и never-raise (TC-01..06). Нет тривиальных/пустых тестов.
- Security: ключ булев, новых поверхностей/секретов нет.
## Документация
Изменён `src/` (поведение исходящих Telegram-запросов) → документация обновлена в
том же PR, как требует CLAUDE.md §2/§6:
- `CHANGELOG.md` — запись в `## [Unreleased]` (тип `fix:`). ✅
- `CLAUDE.md` — раздел «Нотификации / Telegram live-tracker» дополнен пунктом
«Без link-preview (ORCH-080)». ✅
- `docs/architecture/README.md` — компонент Notifications дополнен ремаркой ORCH-080. ✅
- ADR `docs/work-items/ORCH-080/06-adr/ADR-001-disable-telegram-link-preview.md` заведён. ✅
Документация соответствует коду; расхождений нет.

View File

@@ -0,0 +1,66 @@
---
type: test-report
work_item_id: ORCH-080
result: PASS
---
# Test Report — ORCH-080
Подавление Telegram link-preview (`disable_web_page_preview: True`) в `send_telegram`
(`sendMessage`) и `edit_telegram` (`editMessageText`). Сохранены `parse_mode: HTML`,
`disable_notification`, never-raise и контракты возврата.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Дата: 2026-06-09
- Ветка: `feature/ORCH-080-orch-52g-telegram-link-preview`
- Review verdict: APPROVED (`12-review.md`)
## Smoke test API (prod 8500, read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
| `GET /status` | OK (ORCH-080 = task #62, stage `testing`) |
| `GET /queue` | OK (breaker `closed`, preflight_ok, reconcile/reaper enabled) |
## Результаты тестов
| TC ID | Описание | Тест(ы) | Результат |
|-------|----------|---------|-----------|
| TC-01 | `disable_web_page_preview: True` в payload `sendMessage` (AC-1) | `test_send_telegram_disables_link_preview` | PASS |
| TC-02 | `disable_web_page_preview: True` в payload `editMessageText` (AC-2) | `test_edit_telegram_disables_link_preview` | PASS |
| TC-03 | Регрессия `parse_mode: HTML` в обоих payload (AC-4) | `test_send_telegram_keeps_parse_mode_html`, `test_edit_telegram_keeps_parse_mode_html` | PASS |
| TC-04 | Регрессия полей `send_telegram` + проброс `disable_notification` (AC-5) | `test_send_telegram_preserves_existing_fields`, `test_send_telegram_disable_notification_default_false`, `test_edit_telegram_preserves_existing_fields` | PASS |
| TC-05 | Контракты возврата (`message_id`/`None`/`EDIT_OK`) (AC-5/AC-6) | `test_send_telegram_returns_message_id`, `test_send_telegram_returns_none_without_creds`, `test_edit_telegram_returns_edit_ok` | PASS |
| TC-06 | never-raise → `None`/`EDIT_FAILED` без проброса (AC-6) | `test_send_telegram_never_raises`, `test_edit_telegram_never_raises` | PASS |
| TC-07 | Регресс сюиты трекера/уведомлений (bump/edit, ссылки, resilience) | `test_telegram_tracker.py`, `test_tracker_bump.py`, `test_notify_issue_links.py`, `test_resilience.py` (+ `test_link_preview_disabled.py`) — 106 passed | PASS |
| TC-08 | Полная регрессия `pytest tests/ -q` (AC-6) | вся сюита — 1058 passed | PASS |
## Покрытие Acceptance Criteria
- AC-1 — TC-01 ✅
- AC-2 — TC-02 ✅
- AC-3 (баннер исчез в чате) — ручная верификация на staging (8501) после деплоя; автоматически косвенно покрыто AC-1/AC-2 (payload несёт флаг). Не блокирует тест-гейт.
- AC-4 — TC-03 + `test_notify_issue_links.py` зелёный ✅
- AC-5 — TC-04/TC-05 ✅
- AC-6 — TC-06 + полный прогон зелёный ✅
- AC-7 — документация (CHANGELOG/CLAUDE.md/architecture/ADR) проверена на review-стадии ✅
## Вывод pytest
Полная сюита:
```
1058 passed, 1 warning in 26.61s
```
Целевые файлы ORCH-080 (TC-01..07):
```
106 passed, 1 warning in 3.24s
```
(`test_link_preview_disabled.py` — 12 passed.)
Единственный warning — `PydanticDeprecatedSince20` в `src/config.py:5` (предсуществующий, не связан с ORCH-080).
## Итог
**PASS** — все автоматические тесты (TC-01..08) зелёные, smoke API OK, регрессий нет.
Задача готова к переходу на стадию `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-080
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,26 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T22:31:47Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (port 8501).
Run canonically **inside** the container via the Docker exec API (REST equivalent of
`docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py
--base-url http://localhost:8501 --mode stub`), so B6 reads the staging instance's own
process-env registry (ORCH-048, ADR-001).
**Exit code: 0 → advance.** All REAL pipeline checks passed (8/10 PASS).
- Block A (SMOKE): A1 /health, A2 /queue, A3 ORCH_STAGING=true — PASS
- Block B (ACCESS): B4 Plane sandbox, B5 Gitea sandbox (push=true), B6 registry isolation
(sandbox present, prod ET/ORCH absent) — PASS
- Block C (E2E): C7 create issue in SANDBOX, C8 trigger pipeline via /webhook/plane — PASS
- C9a/C9b — FAILED but **waived** (known sandbox-infra checks; depend on SANDBOX bot
accounts being project members, not on the pipeline). Tolerated under ORCH-061 because
every REAL check is green.
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

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-81: конвейер не создаёт PR для ветки → деплой стопорится на merge-verify (HOLD)
Work Item ID: ORCH-082
## Description
TBD

View File

@@ -0,0 +1,119 @@
# 01 — BRD: ORCH-082 (ORCH-81)
**Конвейер не создаёт PR для ветки → деплой стопорится на merge-verify (HOLD)**
- Work Item: **ORCH-082** (Plane-заголовок «ORCH-81»)
- Repo: `orchestrator` (self-hosting)
- Тип: **Багфикс / надёжность конвейера**
- Приоритет: **HIGH** — блокирует автономный деплой
- Зона: создание PR (reviewer/developer/deployer пути), `src/merge_gate.py`, `src/stage_engine.py` (`_handle_merge_verify`), `src/agents/launcher.py` (`_ensure_pr`)
---
## 1. Контекст и проблема
При деплое **ORCH-074** (08.06, статус «Confirm Deploy») детерминированный finalizer
(`run_deploy_finalizer` → под-гейт `_handle_merge_verify`) вызвал
`merge_gate.merge_pr(repo, branch)` и получил **`ok=False` («no open PR»)**: в Gitea для
ветки `feature/ORCH-074-…` **не существовало открытого PR** с `head.ref==branch` и
`base.ref=="main"`.
Защита **ORCH-073** (fail-closed по «SHA-в-main») отработала **корректно**: задача удержана
на стадии `deploy` (НЕ `done`), Plane → Blocked, Telegram-alert, ложно-зелёного `done` не
произошло. Это **правильное** поведение для случая «merge реально невозможен».
**Дефект не в защите, а в инварианте до неё:** автономный конвейер **не гарантировал**, что к
моменту merge у ветки существует открытый PR. PR на сегодня создаётся ровно в одном месте —
`launcher._ensure_pr`, вызываемом **только** на пути `agent == "developer"` и **только** когда
в этом конкретном run был непустой git-diff, успешный commit и успешный push (см. root-cause
ниже). Любой сценарий, где developer-run не произвёл свежий коммит, оставляет ветку **без PR**,
и задача неминуемо застревает на merge-verify.
### Workaround, применённый вручную (НЕ фикс)
PR #79 создан вручную через Gitea API (`mergeable=True`) → штатно перезапущен
`run_deploy_finalizer``merge_pr` честно влил код в `main` → задача `done`. Это разовое ручное
вмешательство, **не** устранение причины.
### Почему это системный пробел, а не разовый сбой
Так как создание PR **не гарантировано конвейером**, любая следующая задача с тем же стечением
обстоятельств (developer-run без нового коммита; тихо упавший вызов создания PR; ветка
восстановлена/пересоздана вручную) застрянет на merge-verify тем же образом. Автономность
деплоя (цель ORCH-54) этим заблокирована.
---
## 2. Root cause (предварительный аудит кода — подтвердить логами G1)
PR создаётся **исключительно** функцией `AgentLauncher._ensure_pr` (`src/agents/launcher.py`),
которая вызывается из `_monitor_agent` по цепочке условий:
```
exit_code == 0
→ есть worktree-изменения (git status --porcelain непусто)
→ git commit succeeded
→ git push succeeded
→ agent == "developer" ←── ТОЛЬКО здесь вызывается self._ensure_pr(...)
```
Отсюда минимум три структурных способа остаться без PR:
- **R-A (условное создание).** Если developer-run завершился без изменений (`git status`
пустой) — ветка уже была закоммичена/запушена в прошлый run, бойнс REQUEST_CHANGES без новых
правок, повторный прогон, или ручное восстановление ветки — `_ensure_pr` **не вызывается
вовсе**. PR не появится никогда. (Соответствует гипотезе ТЗ №2.)
- **R-B (тихий сбой создания).** `_ensure_pr` ловит любое исключение
(`except Exception → logger.error → return None`): транзиентная ошибка Gitea на шаге
`POST …/pulls` теряется без ретрая и без эскалации. Конвейер «думает», что developer
отработал, и едет дальше. (Гипотеза ТЗ №1 — silent fail.)
- **R-C (разъехавшееся состояние ветки/PR).** ORCH-074 — первая задача после серии ручных
восстановлений `main` 08.06. PR мог быть закрыт/пересоздан, либо у ветки остался только
авто-docs-PR (`base != main`), который `merge_pr`/`pr_already_merged` корректно НЕ считают
кодовым PR. (Гипотеза ТЗ №4.)
Идемпотентность (гипотеза №3): сам `_ensure_pr` идемпотентен на чтении (сначала `GET …open&head`,
создаёт только если пусто), но он не запускается вне «свежий developer-коммит», поэтому
идемпотентность не достигает merge-стадии — никакой флаг «PR создан» в БД не хранится.
**Вывод:** гарантия «к моменту merge у ветки есть открытый код-PR» в конвейере **отсутствует**.
---
## 3. Бизнес-цели
| ID | Цель |
|----|------|
| **G1** | Установить и задокументировать точную причину отсутствия PR на ORCH-074 (код-аудит + логи run_id 396/398). |
| **G2** | Гарантировать инвариант: к моменту merge-verify у ветки **есть** открытый код-PR; если его нет — finalizer/deployer создаёт его сам, **идемпотентно**, ПЕРЕД `merge_pr`, вместо HOLD на ручное вмешательство. |
| **G3** | Явно логировать факт PR: **PR-created / PR-existed / PR-create-failed** (наблюдаемость). |
## 4. Не-цели (явные границы)
- НЕ ослаблять защиту ORCH-073: fail-closed по «SHA-в-main» остаётся. Реальная невозможность
merge → по-прежнему HOLD + alert.
- НЕ авто-мержить без PR (PR — обязательный артефакт ревью/слияния).
- НЕ создавать PR в неподходящий момент — только на ребре `deploy → done`, ПОСЛЕ прохождения
всех гейтов (security/merge-gate/staging/image-freshness уже пройдены).
- НЕ менять `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схему БД, контракты `check_deploy_status`,
exit-коды хука.
## 5. Заинтересованные стороны
- **Owner** (homenet542) — автономность деплоя орка.
- Все проекты на инстансе (enduro-trails) — общий прод/очередь: ложный HOLD self-задачи не
должен требовать ручного вмешательства, а реальный дефект merge — обязан удерживаться.
## 6. Бизнес-риски и допущения
- **Грабли (из ORCH-073):** у ветки может быть несколько PR (код-PR + авто docs-PR). Создание/
выбор PR обязан фильтровать `head.ref==branch` И `base.ref=="main"`, иначе слияние/верификация
схватят не тот PR.
- **Допущение:** merge-verify исполняется ПОСЛЕ всех гейтов, поэтому создание PR именно здесь не
обходит ревью и безопасно по времени.
- **Контракт надёжности:** весь новый путь — **never-raise**; ошибка создания PR (Gitea
недоступна) → честный HOLD + alert, а не исключение в `advance_stage`.
## 7. Definition of Done (бизнес-уровень)
1. Root cause задокументирован (`06-adr/` архитектором, ссылка из ADR на этот BRD).
2. После фикса задача с веткой без PR не зависает: конвейер создаёт PR идемпотентно и доводит до
`done` (при честном merge).
3. Защита ORCH-073 цела (регресс-тест на «код не в main» → HOLD).
4. Логи различают created/existed/failed.
5. `pytest` зелёный; never-raise соблюдён.

View File

@@ -0,0 +1,108 @@
# 02 — ТЗ: ORCH-082 (ORCH-81)
**Гарантированный идемпотентный код-PR перед merge-verify + наблюдаемость**
> Машина стадий, реестр `QG_CHECKS`, схема БД, exit-коды хука, контракты
> `check_deploy_status`/`_parse_deploy_status`, защита ORCH-073 (SHA-в-main) — **НЕ меняются**.
> Изменение — точечная врезка «ensure PR» в под-гейт merge-verify + новый идемпотентный
> PR-актор в `merge_gate` + структурное логирование.
---
## 1. Задействованные модули `src/`
| Модуль | Роль в задаче | Характер изменения |
|--------|---------------|--------------------|
| `src/merge_gate.py` | leaf-логика merge-актора (`merge_pr`, `verify_merged_to_main`, `pr_already_merged`) | **+ новый идемпотентный актор** `ensure_open_pr(repo, branch) -> (status, detail)` (never-raise). |
| `src/stage_engine.py` | под-гейт `_handle_merge_verify` на ребре `deploy → done` | **врезка:** вызвать `ensure_open_pr` ПЕРЕД `merge_pr`; на `failed` → честный HOLD+alert; логировать исход. |
| `src/agents/launcher.py` | `_ensure_pr` (текущий единственный создатель PR) | **усилить наблюдаемость** (различать created/existed/failed) — опционально переиспользовать новый актор `merge_gate.ensure_open_pr`, чтобы создание PR было единым кодом. Поведение «создавать только у developer» НЕ ужесточать без необходимости. |
| `src/config.py` | флаги | **+ kill-switch** `merge_verify_autocreate_pr_enabled` (дефолт `True`), область — та же `merge_verify_applies` (self-hosting / `merge_verify_repos`). |
| `docs/architecture/README.md`, `CHANGELOG.md` | golden source | обновить (раздел ORCH-071/073 merge-verify — дописать про авто-создание PR). |
> Точная сигнатура `ensure_open_pr`, имя/дефолт kill-switch и место врезки — за архитектором
> (ADR). Ниже — функциональные требования к поведению, не финальный дизайн.
## 2. Функциональные требования
### FR-1 — Идемпотентный PR-актор `merge_gate.ensure_open_pr(repo, branch)`
Возвращает структурированный исход (например `("existed"|"created"|"failed", detail)`):
1. `GET …/pulls?state=open` → если есть PR с **`head.ref==branch` И `base.ref=="main"`** →
`("existed", <number>)`. **Фильтр идентичен `merge_pr`/ORCH-073 FR-3** — авто-docs-PR
(`base != main`) НЕ считается код-PR.
2. Иначе `POST …/pulls` (`head=branch`, `base=main`, заголовок/тело — авто) → `201`
`("created", <number>)`.
3. Идемпотентность: если параллельно PR уже создан и Gitea вернёт ошибку «PR exists» —
повторный `GET` подтверждает существующий PR и возвращает `("existed", …)`, **дубль не
плодится** (AC-2).
4. Любая иная ошибка HTTP/parse/сети → `("failed", <reason>)`. **Never-raise.**
### FR-2 — Врезка в `_handle_merge_verify` (ребро `deploy → done`)
Внутри существующего `_handle_merge_verify`, ПОСЛЕ `merge_verify_applies(repo)`-гейта и
резолва `validated_revision`, но **ПЕРЕД** `merge_pr`:
- если `merge_verify_autocreate_pr_enabled` → вызвать `ensure_open_pr(repo, branch)`;
- `status == "created"|"existed"` → продолжить штатно к `merge_pr``verify_merged_to_main`;
- `status == "failed"`**честный HOLD + alert** (как сегодняшний not-merged путь:
`note_not_merged_alert` + `set_issue_blocked` + Plane-коммент + Telegram; задача остаётся на
`deploy`, НЕ `done`, БЕЗ отката на development) с сообщением, отражающим «PR создать не
удалось» (а не «PR не влит»).
- kill-switch off → текущее поведение 1:1 (никакого создания PR).
### FR-3 — Защита ORCH-073 цела (регресс-инвариант)
Создание PR **не подменяет** проверку слияния. После `ensure_open_pr` + `merge_pr` верификация
остаётся **только** `verify_merged_to_main` (SHA-в-main, ORCH-073 FR-1) + регресс-гард
(`check_main_regression`). Если код реально не оказался в `main` — HOLD сохраняется. Создание PR
лишь устраняет **ложный** HOLD «no open PR», который конвейер обязан был предотвратить.
### FR-4 — Наблюдаемость (G3)
В лог писать однозначный исход на каждом из мест работы с PR:
- `merge-verify ensure_open_pr -> created PR #N` /
- `… -> existed PR #N` /
- `… -> failed: <reason>`.
Сообщение HOLD при `failed` обязано отличаться текстом от HOLD «not merged» (оператор должен
видеть, что причина — невозможность создать PR, а не невозможность слить уже созданный).
Желательно — пометка исхода в `14-deploy-log.md` (best-effort, frontmatter `deploy_status:`
нетронут).
### FR-5 — Идемпотентность повторного прохода
Повторный заход в merge-verify (reaper / reconciler / повторный approve) при уже существующем
PR → `ensure_open_pr` возвращает `("existed", …)`, `merge_pr``already-merged`/штатно — **без
дублей PR и без побочных эффектов** (INV-5/AC-9 ORCH-073 сохранены).
## 3. Изменения API (HTTP / внутренние)
- **Внешний HTTP API сервиса — без изменений** (новых endpoint нет).
- **Исходящие вызовы Gitea:** новый `POST /api/v1/repos/{owner}/{repo}/pulls` из контекста
merge-verify (тот же вызов, что уже делает `_ensure_pr`); чтение — существующий
`GET …/pulls?state=open`.
- **Внутренний контракт `merge_gate`:** новая публичная функция `ensure_open_pr` (leaf,
never-raise), вызывается из `stage_engine._handle_merge_verify` (и опционально из
`launcher._ensure_pr`).
## 4. Изменения схемы БД
**Нет.** Состояние идемпотентности выводится из самого Gitea (наличие открытого PR), миграции
не требуются. (Согласуется с restart-safe-моделью merge-verify.)
## 5. Требования к новым QG checks
**Новых зарегистрированных QG-checks нет.** Это под-гейт-врезка в `advance_stage`
(`_handle_merge_verify`), как и сам ORCH-071 merge-verify — не отдельный `QG_CHECKS`-элемент.
Реестр `QG_CHECKS` не трогается.
## 6. Конфигурация / kill-switch
- `merge_verify_autocreate_pr_enabled: bool = True` (env `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED`).
`False` → ровно прежнее поведение (нет авто-создания PR; «no open PR» → HOLD как раньше).
- Область действия — `merge_gate.merge_verify_applies(repo)`: реально только для self-hosting /
`merge_verify_repos`; прочие репо — no-op.
## 7. Артефакты pipeline (создать/обновить)
- `docs/work-items/ORCH-082/06-adr/ADR-001-*.md` — архитектор (root cause G1 + дизайн ensure-PR).
- `12-review.md`, `13-test-report.md`, `14/15/16-*` — последующие стадии.
- Обновить `docs/architecture/README.md` (блок ORCH-071/073) и `CHANGELOG.md` — в ТОМ ЖЕ PR
(правило агентов №2/№6).
## 8. Инварианты (не нарушать)
- `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`/`_parse_deploy_status`,
exit-коды хука, terminal-sync, merge-gate (ORCH-043), image-freshness (ORCH-058) — **без
изменений**.
- Контракт **never-raise** на всём пути merge-verify (INV-1 ORCH-073).
- Слияние только через PR (`POST /pulls/{index}/merge`); `main` никогда не push/force-push.
- Защита ORCH-073 (SHA-в-main + регресс-гард) приоритетна: при конфликте «создать PR» проигрывает
«не дать ложно-зелёный done».

View File

@@ -0,0 +1,69 @@
# 03 — Критерии приёмки: ORCH-082 (ORCH-81)
Каждый критерий — однозначное условие PASS/FAIL. Машинные вердикты гейтов — только из
YAML-frontmatter.
---
### AC-1 — Root cause задокументирован
- **PASS:** в `06-adr/ADR-001-*.md` зафиксировано, **почему** PR не создался на ORCH-074
(со ссылкой на код-путь `launcher._ensure_pr` и/или логи run_id 396/398), и какая из гипотез
R-A/R-B/R-C подтвердилась.
- **FAIL:** причина не названа / только догадка без привязки к коду или логам.
### AC-2 — Гарантированный идемпотентный код-PR к merge-verify
- **PASS:** к моменту merge-verify у ветки гарантированно существует открытый PR с
`head.ref==branch` И `base.ref=="main"`; повторный вызов авто-создания при уже существующем PR
**не плодит дубль** (возвращает existed).
- **FAIL:** при отсутствии PR задача сразу уходит в HOLD; ИЛИ повторный проход создаёт второй PR.
### AC-3 — Авто-создание PR ПЕРЕД merge_pr (вместо немедленного HOLD)
- **PASS:** при физическом отсутствии открытого код-PR `_handle_merge_verify` сначала создаёт PR
(`ensure_open_pr → created`), затем выполняет `merge_pr``verify_merged_to_main`; ложного
HOLD «no open PR» не возникает.
- **FAIL:** «no open PR» по-прежнему приводит к HOLD без попытки создать PR (при включённом
kill-switch).
### AC-4 — Защита ORCH-073 цела (регресс)
- **PASS:** при реальном «код не в `main`» (`verify_merged_to_main → False`) — по-прежнему HOLD +
alert + `set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на development. Регресс-гард
`check_main_regression` не ослаблен.
- **FAIL:** создание PR маскирует невлитый код и пропускает задачу в `done`; ИЛИ ослаблен
SHA-в-main / регресс-гард.
### AC-5 — Логи различают исход PR
- **PASS:** в логах присутствует ровно один однозначный исход на проход: **PR-created** /
**PR-existed** / **PR-create-failed**; HOLD по «create-failed» текстуально отличим от HOLD
«not merged».
- **FAIL:** исход не логируется или created/existed/failed неразличимы.
### AC-6 — Грабли мультиPR: фильтр base==main
- **PASS:** при наличии у ветки авто-docs-PR (`base != main`) актор НЕ принимает его за код-PR и
создаёт/выбирает именно PR на `main`.
- **FAIL:** docs-PR трактуется как код-PR (слияние/верификация работают не с тем PR).
### AC-7 — Never-raise + честный HOLD при недоступности Gitea
- **PASS:** при ошибке создания PR (Gitea недоступна/HTTP-ошибка) `ensure_open_pr` возвращает
`failed`, путь merge-verify даёт честный HOLD+alert, исключение НЕ всплывает в `advance_stage`.
- **FAIL:** исключение пробрасывается / процесс падает / задача молча уходит в `done`.
### AC-8 — Kill-switch off → прежнее поведение 1:1
- **PASS:** при `merge_verify_autocreate_pr_enabled=False` авто-создание не выполняется; «no open
PR» → HOLD как до фикса (поведение ORCH-074 воспроизводится).
- **FAIL:** при выключенном флаге PR всё равно создаётся.
### AC-9 — Условность (область self-hosting)
- **PASS:** для не-self репозиториев (`merge_verify_applies → False`) врезка — no-op; создание PR
остаётся за прежним механизмом.
- **FAIL:** авто-создание срабатывает для чужих репо.
### AC-10 — Инварианты не нарушены
- **PASS:** `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, exit-коды хука,
merge-gate/image-freshness — без изменений; `main` не push/force-push; документация
(`README.md`, `CHANGELOG.md`) обновлена в этом же PR.
- **FAIL:** затронут любой из перечисленных инвариантов / документация не обновлена.
### AC-11 — pytest зелёный
- **PASS:** `pytest tests/ -q` зелёный, включая новые тесты из `04-test-plan.yaml` и
существующие `test_merge_verify*.py` / `test_orch073_*` / `test_merge_actor.py`.
- **FAIL:** любой тест падает.

View File

@@ -0,0 +1,90 @@
work_item: ORCH-082
title: "Гарантированный идемпотентный код-PR перед merge-verify (фикс ложного HOLD)"
strategy: >
Юнит-тесты на новый идемпотентный актор merge_gate.ensure_open_pr (мок Gitea HTTP)
и интеграционные тесты на врезку в stage_engine._handle_merge_verify (мок merge_gate
+ verify), включая регресс ORCH-073. Все пути — never-raise. Gitea и git мокаются,
сеть не дёргается.
tests:
# ---- ensure_open_pr: идемпотентный PR-актор (FR-1) ----
- id: TC-01
type: unit
description: "ensure_open_pr: открытого код-PR нет -> POST создаёт PR -> ('created', N); фильтр base==main применён"
module: tests/test_orch082_ensure_pr.py
expected: PASS
- id: TC-02
type: unit
description: "ensure_open_pr: открытый PR head==branch И base==main уже есть -> ('existed', N), POST не вызывается (нет дубля)"
module: tests/test_orch082_ensure_pr.py
expected: PASS
- id: TC-03
type: unit
description: "Грабли мультиPR: у ветки только docs-PR (base!=main) -> он НЕ считается код-PR -> создаётся PR на main (AC-6)"
module: tests/test_orch082_ensure_pr.py
expected: PASS
- id: TC-04
type: unit
description: "ensure_open_pr never-raise: Gitea POST/GET кидает HTTP/timeout -> ('failed', reason), исключение не всплывает (AC-7)"
module: tests/test_orch082_ensure_pr.py
expected: PASS
- id: TC-05
type: unit
description: "Идемпотентность гонки: POST вернул 'PR exists' -> повторный GET подтверждает существующий -> ('existed', N), дубль не создан"
module: tests/test_orch082_ensure_pr.py
expected: PASS
# ---- _handle_merge_verify: врезка ensure-PR (FR-2/FR-3) ----
- id: TC-06
type: integration
description: "merge-verify: PR отсутствовал -> ensure_open_pr создаёт -> merge_pr -> verify True -> deploy->done БЕЗ ложного HOLD (AC-3)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
- id: TC-07
type: integration
description: "Регресс ORCH-073: PR создан/влит, но verify_merged_to_main=False (код не в main) -> HOLD + set_issue_blocked, НЕ done, без отката (AC-4)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
- id: TC-08
type: integration
description: "ensure_open_pr -> 'failed' (Gitea down) -> честный HOLD+alert, текст отличается от 'not merged', advance_stage не падает (AC-7)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
- id: TC-09
type: integration
description: "Kill-switch merge_verify_autocreate_pr_enabled=False -> ensure_open_pr не вызывается, 'no open PR' -> прежний HOLD 1:1 (AC-8)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
- id: TC-10
type: integration
description: "Условность: non-self репо (merge_verify_applies=False) -> врезка no-op, авто-создание не выполняется (AC-9)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
- id: TC-11
type: integration
description: "Идемпотентный повторный проход (reaper/reconciler): PR уже existed, merge_pr=already-merged -> verify True -> done, без дублей PR (AC-2/FR-5)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
# ---- Наблюдаемость (G3 / AC-5) ----
- id: TC-12
type: unit
description: "Логи различают created/existed/failed; HOLD-сообщение create-failed != HOLD-сообщение not-merged (caplog, AC-5)"
module: tests/test_orch082_merge_verify_autocreate.py
expected: PASS
# ---- Регресс существующего merge-verify контракта ----
- id: TC-13
type: integration
description: "Happy-path ORCH-071/073 не изменён: merge_pr ok + verify True + регресс-гард ok -> done, merged_to_main: true во frontmatter"
module: tests/test_merge_verify.py
expected: PASS

View File

@@ -0,0 +1,221 @@
# ADR-001: Гарантированный идемпотентный код-PR перед merge-verify (ensure_open_pr)
- Work Item: **ORCH-082** (Plane-заголовок «ORCH-81»)
- Repo: `orchestrator` (self-hosting)
- Связь: амендмент к merge-verify ([adr-0013](../../../architecture/adr/adr-0013-merge-verify-gate.md),
[adr-0014](../../../architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md));
глобально зафиксировано в [adr-0016](../../../architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md)
- BRD/ТЗ/AC: `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`
## Статус
Accepted
## Контекст
### Что случилось (инцидент ORCH-074, 08.06)
Деплой ORCH-074 встал на под-гейте merge-verify (ребро `deploy → done`):
`run_deploy_finalizer → _handle_merge_verify` вызвал `merge_gate.merge_pr(repo, branch)` и
получил `ok=False, "no open PR"` — в Gitea для ветки `feature/ORCH-074-…` **не было открытого
PR** с `head.ref==branch` И `base.ref=="main"`. Защита ORCH-073 (fail-closed по «SHA-в-main»)
**отработала правильно**: задача удержана на `deploy` (НЕ `done`), Plane → Blocked, Telegram-alert,
ложно-зелёного `done` не произошло. Разблокировано вручную — PR #79 создан через Gitea API,
finalizer перезапущен, код честно влит, задача `done`. Это **workaround, не фикс**.
### Root cause (G1, подтверждён код-аудитом)
PR создаётся в конвейере **ровно в одном месте**`AgentLauncher._ensure_pr`
(`src/agents/launcher.py:1079`), и вызывается он из `_monitor_agent` **только** по цепочке
условий (`src/agents/launcher.py:751753`):
```
exit_code == 0
→ git status --porcelain непусто (есть worktree-изменения)
→ git commit succeeded
→ git push succeeded
→ agent == "developer" ←── ТОЛЬКО здесь вызывается self._ensure_pr(...)
```
Отсюда класс «ветка без PR» структурно неизбежен. Подтверждённые код-аудитом ветви:
- **R-A (условное создание) — структурный первопричинный дефект.** Если в конкретном
developer-run нет свежих изменений (`git status` пуст: ветка уже была закоммичена/запушена
ранее, бойнс REQUEST_CHANGES без новых правок, повторный прогон, **ручное восстановление
ветки**) — `_ensure_pr` **не вызывается вовсе**. PR не появится никогда. Никакого
персистентного флага «PR создан» в БД нет, поэтому идемпотентность чтения внутри `_ensure_pr`
до merge-стадии не доходит.
- **R-C (разъехавшееся состояние ветки/PR) — проксимальный триггер ORCH-074.** ORCH-074 — первая
задача после серии **ручных восстановлений `main` 08.06**: открытый код-PR был закрыт/не
пересоздан, у ветки мог остаться лишь авто-docs-PR (`base != main`), который `merge_pr` (фильтр
`base=="main"`, ORCH-073 FR-3) корректно НЕ считает кодовым.
- **R-B (тихий сбой создания) — потенциальная, не первопричина здесь.** `_ensure_pr` глотает любое
исключение (`except Exception → logger.error → return None`): транзиентная ошибка Gitea на
`POST …/pulls` теряется без ретрая и эскалации.
**Вывод:** в конвейере **отсутствует инвариант** «к моменту merge-verify у ветки есть открытый
код-PR». Защита ORCH-073 верно ловит следствие, но причина — выше по потоку. Любая следующая
задача с тем же стечением обстоятельств застрянет тем же образом → автономный деплой (ORCH-54)
заблокирован.
### Ограничения, которые нельзя нарушать
- Защита ORCH-073 (SHA-в-main + регресс-гард) — приоритетна. Создание PR **не должно** маскировать
реально невлитый код.
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД, `check_deploy_status`/`_parse_deploy_status`,
exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058) — без изменений.
- Весь путь merge-verify — **never-raise**.
- Слияние только через PR; `main` никогда не push/force-push.
## Решение
Закрыть пробел инвариантом «обеспечить открытый код-PR» **внутри того же под-гейта merge-verify**,
ПЕРЕД детерминированным `merge_pr`. Три точечные врезки, симметричные существующему дизайну
ORCH-071/073 (leaf-актор в `merge_gate` + врезка в `_handle_merge_verify` + kill-switch). Машина
стадий и реестры не трогаются.
### Р-1. Новый идемпотентный leaf-актор `merge_gate.ensure_open_pr(repo, branch)`
Сигнатура (решение архитектора по ТЗ §1):
```python
def ensure_open_pr(repo: str, branch: str) -> tuple[str, str]:
"""Гарантировать открытый код-PR (head==branch, base==main). never-raise.
Возврат: ("existed", "<number>") | ("created", "<number>") | ("failed", "<reason>").
"""
```
Алгоритм (FR-1):
1. `GET …/pulls?state=open` → найти PR с **`head.ref==branch` И `base.ref=="main"`**. Фильтр
**идентичен** `merge_pr`/ORCH-073 FR-3 — авто-docs-PR (`base != main`) НЕ считается код-PR
(AC-6). Нашли → `("existed", <number>)`.
2. Иначе `POST …/pulls` (`head=branch`, `base="main"`, авто-заголовок/тело) → `201`
`("created", <number>)`.
3. **Идемпотентность при гонке:** если на `POST` Gitea вернёт «PR exists»/`409`/`422`
повторный `GET` (шаг 1) подтверждает существующий PR → `("existed", …)`. Дубль не плодится
(AC-2, FR-5).
4. Любая иная HTTP/parse/сетевая ошибка → `("failed", <reason>)`. **Never-raise** (`except
Exception → ("failed", str(e))`).
Актор — **leaf** (зависит только от `settings` + `httpx`, без импорта `stage_engine`), как
`merge_pr`/`verify_merged_to_main`. Таймауты — переиспользовать `settings.merge_pr_timeout_s`
(тот же класс Gitea-вызовов).
> **Почему фильтр `base=="main"` критичен** (грабли ORCH-073): у ветки одновременно бывают код-PR
> и авто-docs-PR. Без фильтра актор «увидит» docs-PR как existed и не создаст нужный код-PR, а
> `merge_pr` потом не найдёт что мержить → петля. Один и тот же предикат `head==branch &&
> base=="main"` гарантирует, что `ensure_open_pr` и `merge_pr` работают с одним и тем же PR.
### Р-2. Врезка в `_handle_merge_verify` (ребро `deploy → done`)
В существующем `_handle_merge_verify` (`src/stage_engine.py:1324`), **ПОСЛЕ**
`merge_verify_applies(repo)`-гейта и резолва `sha = image_freshness.validated_revision(...)`,
но **ПЕРЕД** `merge_pr`:
```python
sha = image_freshness.validated_revision(repo, branch)
# ORCH-082: гарантировать открытый код-PR ДО детерминированного merge_pr.
if settings.merge_verify_autocreate_pr_enabled:
pr_status, pr_detail = merge_gate.ensure_open_pr(repo, branch)
logger.info(
f"Task {task_id}: merge-verify ensure_open_pr -> {pr_status} ({pr_detail})"
)
if pr_status == "failed":
return _hold_pr_create_failed(
task_id, repo, work_item_id, branch, pr_detail, result
)
# "created" | "existed" -> штатно продолжаем к merge_pr.
merged_ok, merge_msg = merge_gate.merge_pr(repo, branch)
...
```
Семантика (FR-2):
- `created | existed` → продолжаем штатно к `merge_pr``verify_merged_to_main`регресс-гард.
- `failed`**честный HOLD + alert** через новый helper `_hold_pr_create_failed` (см. Р-3); задача
остаётся на `deploy` (НЕ `done`), БЕЗ отката на development — симметрично текущему not-merged/
regressed HOLD.
- kill-switch off → блок пропускается целиком → поведение 1:1 как до фикса (AC-8).
Место выбрано так, что **никакой существующий шаг не сдвигается**: `merge_pr` и
`verify_merged_to_main` остаются на своих местах с теми же контрактами. Создание PR — это только
страховка инварианта ДО них.
### Р-3. Новый HOLD-helper `_hold_pr_create_failed` (распознаваемость причины, FR-4/AC-5)
Зеркало существующего `_hold_main_regressed` (`src/stage_engine.py:1280`). Текст HOLD **обязан
отличаться** от not-merged HOLD: оператор должен видеть, что причина — **невозможность создать
PR** (Gitea недоступна), а не **невозможность слить уже созданный**:
```python
def _hold_pr_create_failed(task_id, repo, work_item_id, branch, reason, result) -> bool:
merge_gate.note_not_merged_alert(work_item_id) # переиспользуем счётчик-нотификатор
msg = (f"PR создать не удалось: {reason} (repo={repo}, branch={branch}, "
f"wi={work_item_id}). Открытый код-PR отсутствует и не создан — задача "
f"удержана на `deploy` (НЕ done). Нужно проверить доступность Gitea / создать PR.")
# set_issue_blocked + plane_add_comment + send_telegram (каждый в try/except, never-break HOLD)
result.alerted = True
result.note = "pr-create-failed-hold" # отличается от "merge-not-verified-hold"
result.advanced = False
return True
```
Это сохраняет инвариант «никогда не пробрасываем исключение в `advance_stage`»: `failed`
структурированный исход, а не throw.
### Р-4. Единый источник кода создания PR (опционально, рекомендуется)
`launcher._ensure_pr` рекомендуется **делегировать** в `merge_gate.ensure_open_pr`, чтобы создание
PR жило в одном месте и одинаково логировало created/existed/failed (G3). **Поведенческий
инвариант:** триггер «создавать PR только в developer-пути со свежим коммитом» **НЕ ужесточается**
(BRD/ТЗ §1) — меняется лишь реализация под капотом, не условие вызова. Это снижает риск
рассинхрона двух копий логики «выбрать/создать PR». Если делегирование увеличивает диффу/риск —
допустимо оставить `_ensure_pr` как есть и лишь усилить его логирование (created/existed/failed);
функциональная цель ORCH-082 достигается врезкой Р-2 независимо.
### Р-5. Kill-switch и область действия
- `merge_verify_autocreate_pr_enabled: bool = True`
(env `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED`) в `src/config.py`, рядом с
`merge_verify_enabled`/`regression_guard_enabled`.
- `False` → ровно прежнее поведение: авто-создания нет, «no open PR» → HOLD как в ORCH-074 (AC-8).
- Область — `merge_gate.merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); прочие
репо — no-op, создание PR остаётся за прежним механизмом (AC-9). Отдельного `*_repos` для
авто-создания НЕ вводим: семантически оно неотделимо от merge-verify, у которого уже есть область.
## Последствия
### Плюсы
- Закрыт структурный пробел: к merge-verify ветка гарантированно имеет открытый код-PR; ложный
HOLD «no open PR» больше не требует ручного вмешательства (AC-2/AC-3).
- Защита ORCH-073 цела и приоритетна: верификация остаётся **только** `verify_merged_to_main`
(SHA-в-main) + `check_main_regression`. Реально невлитый код → HOLD как прежде (AC-4/FR-3).
- Идемпотентность по факту Gitea (наличие открытого PR), без новой колонки/таблицы — согласуется с
restart-safe-моделью merge-verify; повторный заход (reaper/reconciler/re-approve) → `existed`,
дублей нет (FR-5/AC-2).
- Распознаваемые исходы в логах и в HOLD-тексте: created / existed / failed (G3/AC-5).
- Инварианты сохранены: `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
exit-коды хука, merge-gate, image-freshness — не тронуты (AC-10). `main` не push/force-push.
### Минусы / ограничения
- Auto-создание PR на ребре `deploy → done` означает, что код-PR может появиться **после** того,
как все гейты (security/merge-gate/staging/image-freshness) уже пройдены по ветке. Это безопасно
по времени (BRD §6 допущение): ревью/гейты валидируют **код ветки**, а PR — лишь механизм
слияния; merge-verify исполняется ПОСЛЕ всех гейтов. PR здесь не обходит ревью.
- При недоступности Gitea задача попадёт в HOLD (как и сегодня) — но теперь с явным текстом
«PR создать не удалось» вместо «PR не влит». Это сознательный fail-closed (AC-7): never-raise,
честный HOLD, не ложно-зелёный `done`.
- Небольшое дублирование Gitea-вызовов между `ensure_open_pr` и `merge_pr` (оба GET список PR). Это
приемлемо: два независимых leaf-актора с одинаковым фильтром важнее микро-оптимизации; объединять
в один вызов — увеличить связность без пользы.
### Влияние на self-hosting
Изменение строго аддитивно и под kill-switch (`True`). Прод-контейнер не рестартится этой задачей;
выкат — через staging-гейт (8501) как любая ORCH-задача. На ребре `deploy → done` риск-профиль не
растёт: при любом сбое — HOLD, не падение `advance_stage`, конвейер всех проектов не встаёт.
## Связанные документы
- BRD/ТЗ/AC: `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`
- Тех-риски: `10-tech-risks.md`
- Глобальный амендмент: [adr-0016](../../../architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md)
- Контекст merge-verify: [adr-0013](../../../architecture/adr/adr-0013-merge-verify-gate.md),
[adr-0014](../../../architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md)
- Постмортем фантомного merge: `docs/history/LESSONS_2026-06-08_phantom-merge.md`,
runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md`

View File

@@ -0,0 +1,27 @@
# 10 — Технические риски: ORCH-082 (ORCH-81)
Риски точечной врезки «ensure_open_pr перед merge-verify». Все — в зоне ребра `deploy → done`
(self-hosting), под kill-switch `merge_verify_autocreate_pr_enabled`.
| ID | Риск | Вероятн. | Влияние | Митигация |
|----|------|----------|---------|-----------|
| **R1** | `ensure_open_pr` выбирает/создаёт **не тот** PR (авто-docs-PR `base != main`) → `merge_pr` мержит/верифицирует не тот PR | Сред. | Высокое | Фильтр `head.ref==branch` И `base.ref=="main"`, **идентичный** `merge_pr` (ORCH-073 FR-3). Тест AC-6: ветка с docs-PR (`base!=main`) → актор его игнорирует и создаёт код-PR на `main`. |
| **R2** | Создание PR **маскирует** реально невлитый код → ложно-зелёный `done` (регресс ORCH-073) | Низк. | Критич. | Верификация остаётся ТОЛЬКО `verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` НЕ влияет на вердикт merge. Регресс-тест AC-4: `verify_merged_to_main→False` ⇒ HOLD, не `done`. |
| **R3** | Гонка: параллельно создаётся 2 PR → дубль | Низк. | Сред. | Идемпотентность FR-1.3: на ошибку «PR exists»/409/422 — повторный GET → `existed`; PR создаётся только если GET пуст. Тест AC-2. |
| **R4** | Исключение из `ensure_open_pr` пробрасывается в `advance_stage` → падение перехода | Низк. | Высокое | Контракт never-raise (`except Exception → ("failed", reason)`); врезка обёрнута внешним try/except `_handle_merge_verify`. `failed` → структурированный HOLD, не throw. Тест AC-7. |
| **R5** | Gitea недоступна на ребре `deploy → done` → задача в HOLD | Низк. | Сред. | Сознательный fail-closed: `failed` → честный HOLD+alert (`_hold_pr_create_failed`), НЕ ложный `done`. Текст HOLD отличим от not-merged (AC-5) — оператор видит причину. Reaper/reconciler/re-approve переиграют, когда Gitea вернётся (FR-5). |
| **R6** | Оператор не различит HOLD «PR не создан» и HOLD «PR не влит» | Сред. | Низк. | Отдельный helper `_hold_pr_create_failed` с собственным текстом и `result.note="pr-create-failed-hold"` (≠ `merge-not-verified-hold`); лог-строка `ensure_open_pr -> failed: <reason>`. AC-5. |
| **R7** | Расхождение логики выбора/создания PR между `launcher._ensure_pr` и `merge_gate.ensure_open_pr` | Сред. | Сред. | Рекомендованное делегирование `_ensure_pr → ensure_open_pr` (единый код). Если не делегируем — обе копии используют ОДИН фильтр `head==branch && base==main`; тест на согласованность. |
| **R8** | Включение по умолчанию (`True`) меняет прод-поведение скрытно | Низк. | Сред. | Поведение строго аддитивно: при наличии PR → `existed`/no-op; меняется лишь ранее-падавший путь «no open PR». Kill-switch `False` → 1:1 ORCH-074 (AC-8). Выкат через staging-гейт (8501). |
| **R9** | Регресс инвариантов (`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/exit-коды) | Низк. | Высокое | Под-гейт-врезка в `advance_stage`, НЕ новый `QG_CHECKS`-элемент и НЕ новая стадия; БД не трогается (идемпотентность из Gitea). Тест AC-10 + полный `pytest`. |
## Зоны без изменений (подтверждение границ)
- **Инфраструктура/топология** — без изменений → `07-infra-requirements.md` не требуется.
- **Схема БД** — без изменений (идемпотентность выводится из Gitea) → `08-data-requirements.md`
не требуется.
- `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука,
merge-gate (ORCH-043), image-freshness (ORCH-058), terminal-sync — не тронуты.
## Главный архитектурный приоритет
При любом конфликте «создать PR» **проигрывает** «не дать ложно-зелёный `done`» (защита ORCH-073).
Создание PR — страховка инварианта ДО merge_pr, никогда не подмена верификации merge.

View File

@@ -0,0 +1,65 @@
---
type: review
work_item_id: ORCH-082
verdict: APPROVED
version: 1
---
# Review ORCH-082 — Гарантированный идемпотентный код-PR перед merge-verify
## Summary
Изменение закрывает отсутствующий инвариант «к моменту merge-verify у ветки есть открытый
код-PR» (root cause ложного HOLD «no open PR» на деплое ORCH-074). Реализовано строго аддитивно,
по дизайну ADR-001: новый идемпотентный leaf-актор `merge_gate.ensure_open_pr`, точечная врезка в
`stage_engine._handle_merge_verify` ПЕРЕД `merge_pr`, distinguishable HOLD-helper
`_hold_pr_create_failed`, делегирование `launcher._ensure_pr` в единый актор, kill-switch
`merge_verify_autocreate_pr_enabled`. Защита ORCH-073 (SHA-в-main + регресс-гард) не ослаблена и
остаётся приоритетной. Машина стадий, `QG_CHECKS`, схема БД, контракты деплоя — не тронуты.
Все 4 оси проверки пройдены:
- **ТЗ (02-trz.md):** FR-1..FR-5 реализованы — идемпотентный актор с фильтром
`head==branch & base=="main"`, врезка после `validated_revision` и до `merge_pr`, честный HOLD
на `failed`, защита ORCH-073 цела, идемпотентность повторного прохода.
- **AC (03-acceptance-criteria.md):** AC-1..AC-11 покрыты. Root cause задокументирован в ADR
(R-A структурный + R-C проксимальный для ORCH-074); идемпотентность/existed (TC-02, TC-05);
autocreate до merge_pr (TC-06); защита ORCH-073 (TC-07); логи различают исход (TC-12); фильтр
base==main (TC-03); never-raise (TC-04, TC-08); kill-switch off (TC-09); условность non-self
(TC-10); инварианты + документация; pytest зелёный.
- **ADR:** реализация 1:1 соответствует Р-1..Р-5 ADR-001; не нарушает глобальные adr-0013/0014
(амендмент adr-0016 корректно зарегистрирован).
- **Качество кода:** never-raise соблюдён (все внешние вызовы в try/except), docstrings на
публичных функциях, тесты содержательные (мок Gitea HTTP + интеграционные на под-гейт, не
тривиальные). Секреты не хардкодятся (token из settings). `main` не push/force-push.
`pytest tests/ -q`**1046 passed**. Целевые наборы (`test_orch082_ensure_pr.py`,
`test_orch082_merge_verify_autocreate.py`, `test_merge_verify.py`) — зелёные.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
### P3 — Nice-to-have
- [ ] Поведенческое уточнение `launcher._ensure_pr`: после делегирования в `ensure_open_pr`
developer-путь теперь требует `base=="main"` (раньше принимался любой открытый PR с
`head==branch`). Это корректное усиление (выравнивание с `merge_pr`) и для штатного потока
PR всегда создаётся на `main`регресса нет; зафиксировано для истории, действий не требует.
## Документация
Документация обновлена в том же PR — соответствие правилу №2/№6 CLAUDE.md:
- `docs/architecture/README.md` — добавлен раздел ORCH-082 в блок merge-verify (строки 209-240).
- `CHANGELOG.md` — запись в `## [Unreleased]`.
- `.env.example``ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=true` + комментарий.
- `docs/architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md` — сквозной ADR (амендмент
adr-0013/0014), зарегистрирован в `docs/architecture/adr/README.md` (макс. номер → 0016).
- `docs/work-items/ORCH-082/06-adr/ADR-001-*.md` — детальный ADR (root cause + дизайн).
- API сервиса не менялось (новых endpoint нет), конфиг-флаг отражён в `.env.example`. Все
изменения `src/` (merge_gate, stage_engine, launcher, config) задокументированы.
**Вердикт: APPROVED** — P0/P1 отсутствуют, документация обновлена, тесты зелёные.

View File

@@ -0,0 +1,81 @@
---
type: test-report
work_item_id: ORCH-082
result: PASS
---
# Test Report — ORCH-082
Гарантированный идемпотентный код-PR перед merge-verify (фикс ложного HOLD «no open PR»).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: feature/ORCH-082-orch-81-pr-merge-verify-hold
- Дата: 2026-06-09
- Review verdict: APPROVED (12-review.md, P0/P1 отсутствуют)
## Проверка окружения
- `GET /health``{"status":"ok","service":"orchestrator"}` — прод-контейнер 8500 жив.
- Тесты прогнаны в worktree ветки (прод не затронут, деструктивных операций нет).
## Smoke test API (prod 8500)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok"}` — OK |
| `GET /status` | OK — ORCH-082 (id=61) виден на стадии `testing` |
| `GET /queue` | OK — `running:1, queued:0`, breaker `closed`, reconcile/reaper/post_deploy активны |
## Результаты (привязка к 04-test-plan.yaml)
| TC ID | Тип | Описание | Тест | Результат |
|-------|-----|----------|------|-----------|
| TC-01 | unit | ensure_open_pr: PR нет → POST создаёт → ('created', N); фильтр base==main | test_tc01_creates_pr_when_absent | PASS |
| TC-02 | unit | PR head==branch И base==main уже есть → ('existed', N), POST не вызывается | test_tc02_existed_no_duplicate | PASS |
| TC-03 | unit | Мульти-PR: только docs-PR (base!=main) → создаётся PR на main (AC-6) | test_tc03_docs_pr_not_counted_creates_on_main | PASS |
| TC-04 | unit | never-raise: GET/POST кидает ошибку → ('failed', reason), не всплывает (AC-7) | test_tc04_never_raise_on_get_error / _on_post_error / _failed_when_post_non_2xx | PASS (3) |
| TC-05 | unit | Гонка: POST 'PR exists' (409/422) → повторный GET → ('existed', N), без дубля | test_tc05_race_post_conflict_confirms_existing[409,422] | PASS (2) |
| TC-06 | integration | PR отсутствовал → ensure создаёт → merge_pr → verify True → done без HOLD (AC-3) | test_tc06_autocreate_then_merge_then_done | PASS |
| TC-07 | integration | Регресс ORCH-073: verify=False → HOLD + set_issue_blocked, НЕ done, без отката (AC-4) | test_tc07_verify_false_still_holds | PASS |
| TC-08 | integration | ensure → 'failed' (Gitea down) → честный HOLD+alert, текст ≠ 'not merged' (AC-7) | test_tc08_ensure_failed_holds_distinct | PASS |
| TC-09 | integration | Kill-switch off → ensure не вызывается, 'no open PR' → прежний HOLD 1:1 (AC-8) | test_tc09_killswitch_off_no_autocreate | PASS |
| TC-10 | integration | Условность: non-self репо (applies=False) → no-op, авто-создание не выполняется (AC-9) | test_tc10_non_self_repo_noop | PASS |
| TC-11 | integration | Идемпотентный повторный проход: PR existed, already-merged → verify True → done (FR-5) | test_tc11_idempotent_redrive | PASS |
| TC-12 | unit | Логи различают created/existed/failed; HOLD create-failed ≠ HOLD not-merged (AC-5) | test_tc12_logs_distinguish_outcomes | PASS |
| TC-13 | integration | Happy-path ORCH-071/073 не изменён: verify True → done, merged_to_main: true | test_merge_verify.py (verify_true_when_sha_is_ancestor + 7 регресс-тестов) | PASS |
Все 13 TC из тест-плана покрыты и зелёные.
## Сопоставление с критериями приёмки (03-acceptance-criteria.md)
- **AC-1** Root cause в ADR-001 (R-A структурный + R-C для ORCH-074) — подтверждено review.
- **AC-2** Идемпотентный код-PR, без дублей — TC-02, TC-05, TC-11 — PASS.
- **AC-3** Авто-создание PR ПЕРЕД merge_pr — TC-06 — PASS.
- **AC-4** Защита ORCH-073 цела (verify=False → HOLD, не done) — TC-07 + test_merge_verify — PASS.
- **AC-5** Логи различают исход PR — TC-12 — PASS.
- **AC-6** Фильтр base==main (docs-PR не код-PR) — TC-03 — PASS.
- **AC-7** Never-raise + честный HOLD при недоступности Gitea — TC-04, TC-08 — PASS.
- **AC-8** Kill-switch off → поведение 1:1 — TC-09 — PASS.
- **AC-9** Условность self-hosting — TC-10 — PASS.
- **AC-10** Инварианты не нарушены, документация обновлена — подтверждено review (README/CHANGELOG/.env.example/ADR).
- **AC-11** pytest зелёный — **1046 passed** — PASS.
## Вывод pytest
Полный прогон:
```
1046 passed, 1 warning in 25.57s
```
(единственный warning — PydanticDeprecatedSince20 в src/config.py:5, не относится к ORCH-082, предсуществующий.)
Целевые наборы:
```
tests/test_orch082_ensure_pr.py ............ (8 passed)
tests/test_orch082_merge_verify_autocreate.py ....... (7 passed)
tests/test_merge_verify.py ........ (8 passed)
======================== 23 passed, 1 warning in 0.42s =========================
```
## Итог
**PASS** — все 1046 тестов зелёные, целевые наборы ORCH-082 + регресс merge-verify зелёные,
smoke API (health/status/queue) OK, все 13 TC и AC-1..AC-11 покрыты. Задача готова к переходу
на стадию `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-082
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-86: reconciler шлёт в Telegram «ET-002 done разблокирована (потерян webhook)» периодически
Work Item ID: ORCH-086
## Description
TBD

View File

@@ -0,0 +1,70 @@
# 01-BRD — ORCH-086: reconciler шлёт ложное «ET-002 done разблокирована»
Work Item: **ORCH-086**
Тип: **Багфикс** (шум уведомлений / остаток livelock)
Приоритет: **MEDIUM**
Зона: `src/reconciler.py`
Связано: продолжение **ORCH-068** (тот фикс задеплоен, но НЕ закрыл этот путь), наследует контракты **ORCH-053 / ORCH-060 / ORCH-066**.
## 1. Контекст / проблема
В Telegram периодически (а особенно сразу после рестарта оркестратора) прилетает уведомление:
> 🔧 reconciler: ET-002 done разблокирована (потерян webhook)
Это **ложный шум**: задача `ET-002` (проект enduro-trails) давно завершена, реально ничего не разблокируется. Уведомление вводит наблюдателя в заблуждение (создаёт впечатление, что конвейер чинит застрявшую задачу, хотя ничего не происходит).
ORCH-068 уже починил аналогичный livelock на **F-2 (plane-side)**: добавил per-issue терминал-исключение (`_is_terminal_state`, группа Plane `completed`/`cancelled`) и in-memory dedup-guard по `issue_id→state_uuid`. Однако эти две защиты **не покрывают путь F-1 (gate-side)**.
## 2. Диагностика (код-аудит, golden source — текущий `src/reconciler.py`)
Уведомление отправляет `Reconciler._note_unblock()` (`reconciler.py` ~стр.444) через `send_telegram()` при `settings.reconcile_notify_unblock=True`.
Два механизма ORCH-068, которые ДОЛЖНЫ были его подавить, на пути F-1 не работают:
1. **Dedup-guard не срабатывает.** Guard ключуется по `state_uuid` и активен только когда `state_uuid is not None` (`_note_unblock`, стр.459463). Но вызов в F-1 (`_reconcile_gate_task`, стр.228):
```python
self._note_unblock(task.get("work_item_id") or str(task_id), stage)
```
передаёт **только 2 аргумента, БЕЗ `state_uuid`** → ветка dedup пропускается → уведомление шлётся при каждом релевантном тике/старте. (В отличие от F-2, где все 4 вызова `_note_unblock` передают `state_uuid` — стр.394/400/407/416.)
2. **Терминал-скип не ловит этот путь.** Терминал-исключение ORCH-068 (`_is_terminal_state`, стр.327344) вызывается **только в F-2** (`_reconcile_plane_issue`, стр.362). В F-1 единственный «терминал-фильтр» — это `get_active_tasks_for_reconcile()` (`db.py` стр.193: `WHERE stage != 'done'`), который смотрит **только на стадию задачи в БД оркестратора** и НЕ знает о статусе задачи в Plane (группа `completed`/`cancelled`). Поэтому задача, которая в БД оркестратора стоит на НЕ-`done` стадии (дрейф), а в Plane уже `Done`, проходит фильтр.
### Почему `advance_if_gate_passed` считает ET-002 «продвинувшейся» (G1 — гипотеза, требует подтверждения в development)
Для enduro-trails (не self-hosting) условные гейты (`check_staging_status`, `check_deploy_status`, merge-gate, image-freshness, security-gate, merge-verify) — **no-op `(True, ...)`** (условность ORCH-35/43/58/71). Поэтому для enduro-задачи, чья стадия в БД оркестратора НЕ `done`, но застряла перед терминалом (например `deploy`), `advance_if_gate_passed` находит гейт зелёным (no-op) → вызывает `advance_stage(..., finished_agent=None)` → возвращает `result.advanced=True` (стр.227) → доходит до `_note_unblock`. Guard 2 (`_is_blocked_or_needs_input`, стр.230) задачу не спасает: его `skip_set` = `{blocked, needs_input, extra_waits}` и **НЕ содержит `done`/`cancelled`** → терминальная-в-Plane задача через него проходит. «Периодичность / при старте» объясняется отсутствием dedup (state_uuid не передан) + чистым in-memory состоянием нового процесса после рестарта (первый проход снова находит задачу).
> **Открытый вопрос для G1 (подтвердить в development по prod-БД/логам):** точная стадия `ET-002` в БД оркестратора в момент срабатывания (в quoted-сообщении фигурирует слово «done», но `get_active_tasks_for_reconcile` исключает `stage='done'` — значит стадия в БД иная либо аномальная). Фикс обязан быть **робастным независимо** от точной стадии: терминальность определяется по группе статуса Plane (как `_is_terminal_state`), а не по строковому совпадению стадии.
## 3. Бизнес-цели
- **G1.** Установить и задокументировать, почему F-1 (`advance_if_gate_passed`) доводит терминальную в Plane задачу (ET-002) до `_note_unblock` на каждом релевантном тике/старте.
- **G2.** Не слать unblock-уведомление для задач, УЖЕ терминальных (`done`/`cancelled`) в Plane (по группе статуса) и/или в оркестраторе — распространить терминал-скип ORCH-068 на путь F-1 (стр.228), а не только на F-2.
- **G3.** Передавать `state_uuid` в `_note_unblock` на **всех** путях (включая F-1) → in-memory dedup-guard работает везде (страховка от повтора, даже если терминал-скип когда-то не сработает).
## 4. Объём (Scope)
**В объёме:**
- Точечная правка `src/reconciler.py`: терминал-скип на пути F-1 + проброс `state_uuid` в `_note_unblock` из F-1.
- Сохранение/корректное инкрементирование наблюдаемости ORCH-068 (`skipped_terminal_total`, `deduped_total`, `unblocked_total`).
- Unit-тесты, покрывающие AC-1…AC-5.
- Обновление документации (`docs/architecture/README.md` блок Reconciler, `CHANGELOG.md`).
**Вне объёма (Не-цели):**
- НЕ ломать легитимный replay реально застрявшей задачи (когда реконсиляция её ДЕЙСТВИТЕЛЬНО двигает — уведомление полезно).
- НЕ трогать пайплайн / статусы enduro-trails.
- НЕ отключать `reconcile_notify_unblock` глобально (потеряем полезные алерты) — подавление **точечное**, только для терминальных.
- НЕ менять `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схему БД, контракты `advance_stage` / `advance_if_gate_passed`.
- НЕ менять поведение F-2 (там ORCH-068 уже корректен) сверх необходимого переиспользования хелперов.
## 5. Заинтересованные лица
- **Owner / Слава** — наблюдатель Telegram-карточек и алертов; страдающая сторона (шум).
- **enduro-trails** — проект, чьи терминальные задачи генерируют ложные алерты; пайплайн не должен быть затронут.
- **orchestrator (self-hosting)** — терминал-детект должен корректно работать и для self (разные наборы Plane-статусов).
## 6. Риски и ограничения
- **R1 (грабли мультипроектности).** enduro-trails и orchestrator — разные проекты с разными наборами Plane-статусов. Терминал-детект ОБЯЗАН работать для обоих: первичный дискриминатор — группа статуса Plane (`completed`/`cancelled`, project-independent), fallback — логические ключи `done`/`cancelled` (как в существующем `_is_terminal_state`, стр.338344).
- **R2 (наблюдаемость).** Нельзя сломать счётчики ORCH-068. При скипе терминальной задачи в F-1 — инкрементировать `skipped_terminal_total` (единая семантика с F-2). `deduped_total`/`unblocked_total` — без регрессии.
- **R3 (never-raise).** Тик реконсилятора обязан оставаться never-raise (сеть Plane может быть недоступна). Сбой терминал-проверки → консервативное поведение (как Guard 2: при ошибке скорее НЕ слать, чем слать ложно; но НЕ ценой подавления легитимного unblock — см. AC-4).
- **R4 (доп. сетевой вызов).** F-1 для проброса `state_uuid` и терминал-детекта должен знать текущий Plane-статус issue. Guard 2 (`_is_blocked_or_needs_input`) уже делает `fetch_issue_state`. Желательно переиспользовать один fetch, не удваивая обращения к Plane API на тик (производительность горячего цикла).
- **R5 (ложно-отрицательный риск).** Слишком агрессивное подавление может задушить полезный алерт о реально застрявшей задаче → обязателен регресс-тест AC-4.
## 7. Метрика успеха
- В Telegram больше нет периодического «ET-002 done разблокирована»; `skipped_terminal_total` растёт (наблюдаемо в `GET /queue`).
- `pytest tests/ -q` зелёный; новые тесты AC-1…AC-5 проходят.

View File

@@ -0,0 +1,68 @@
# 02-TRZ — ORCH-086: терминал-скип и dedup на пути F-1 реконсилятора
> Техническое задание. Архитектурное решение (КАК именно) — за архитектором (ADR). Здесь — ЧТО должно измениться и инварианты.
## 1. Задействованные модули `src/`
- **`src/reconciler.py`** — основной (и, как ожидается, единственный) изменяемый модуль:
- `Reconciler._reconcile_gate_task` (стр.180228) — путь F-1, где находится баг.
- `Reconciler._note_unblock` (стр.444477) — точка отправки уведомления + dedup-guard.
- `Reconciler._is_terminal_state` (стр.327344) — существующий терминал-детект (сейчас зовётся только из F-2); переиспользуется в F-1.
- `Reconciler._is_blocked_or_needs_input` (стр.230288) — уже делает `fetch_issue_state`; желательно переиспользовать его результат, чтобы не удваивать сетевой вызов.
- **Возможно затрагиваемые (read-only переиспользование, без изменения контракта):** `src/plane_sync.py` (`fetch_issue_state`, `get_project_states`, `get_project_state_groups`), `src/projects.py` (`get_project_by_repo`). Изменять их не требуется.
- **НЕ затрагиваются:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), `src/stage_engine.py` (`advance_stage`/`advance_if_gate_passed`), `src/db.py` (схема), `src/config.py` (новые флаги не вводятся).
## 2. Требуемые изменения (функциональные)
### TR-1 (G2): терминал-скип на пути F-1
В `_reconcile_gate_task` ДО вызова `_note_unblock` (а лучше — до/вместо доведения терминальной задачи до `advance_if_gate_passed`) добавить проверку: **является ли задача терминальной**.
- Терминальность определяется тем же способом, что и в F-2 (`_is_terminal_state`): первичный дискриминатор — **группа статуса Plane** issue ∈ `{completed, cancelled}`; fallback (группа недоступна) — логические ключи `done`/`cancelled` проекта. Это покрывает грабли R1 (enduro vs orchestrator).
- Дополнительно: терминальной считается и задача, чья **стадия в БД оркестратора**`{done, cancelled}` (на случай дрейфа Plane↔БД; `get_active_tasks_for_reconcile` уже отсекает `done`, но `cancelled` — нет).
- Терминальная задача → **return без advance и без `_note_unblock`**; инкремент `self.skipped_terminal_total` (единая семантика с F-2, стр.363).
- Скип **безусловный** (как терминал-скип F-2 — без отдельного kill-switch). Это НЕ маскирует легитимный replay: реально застрявшая задача терминальной в Plane не бывает.
> **Где именно** ставить проверку (до `advance_if_gate_passed` или внутри/перед `_note_unblock`) — решает архитектор. Рекомендация: ставить как ранний guard в `_reconcile_gate_task` рядом с Guard 1/Guard 2 (чтобы терминальная задача даже не запускала `advance_if_gate_passed`/гейт). Если терминал-детект требует Plane-статус, он логично переиспользует fetch из Guard 2.
### TR-2 (G3): проброс `state_uuid` в `_note_unblock` из F-1
Вызов на стр.228 должен передавать `state_uuid` (текущий Plane-state issue), чтобы in-memory dedup-guard (`_unblock_dedup`, стр.459463) работал и на пути F-1:
```python
# было:
self._note_unblock(task.get("work_item_id") or str(task_id), stage)
# должно (концептуально):
self._note_unblock(task.get("work_item_id") or str(task_id), stage, state_uuid)
```
- `state_uuid` — текущий uuid статуса issue в Plane (тот же, что используется для терминал-детекта TR-1).
- Если Plane недоступен и `state_uuid` достоверно получить нельзя → допустимо передать `None` (dedup деградирует в no-op, как сегодня), НО приоритетно сначала отрабатывает терминал-скип TR-1; never-raise сохраняется.
- **Сигнатуру `_note_unblock` не менять** (3-й параметр `state_uuid` уже опциональный, стр.445).
### TR-3: переиспользование сетевого вызова (R4, нефункц., желательно)
F-1 не должен делать > 1 обращения к Plane API на задачу за тик ради статуса. `_is_blocked_or_needs_input` уже вызывает `fetch_issue_state`. Архитектор решает форму переиспользования (например, вынести резолв `(project_states, groups, current_state_uuid)` в один helper, питающий Guard 2 + терминал-скип TR-1 + dedup TR-2). Допустимо и без рефакторинга, если число вызовов на тик не растёт значимо.
## 3. Контракты и инварианты (НЕ нарушать)
- **never-raise:** каждая единица работы F-1 изолирована (`_reconcile_gate_task` уже под `try/except` в `reconcile_gate_once`, стр.162168). Любая ошибка терминал-детекта/fetch → не падает тик; консервативное поведение (R3): при невозможности достоверно определить терминальность — НЕ слать ложно, но и не глушить легитимный (см. AC-4: легитимный unblock — это реальная смена стадии не-терминальной задачи; терминал-неопределённость к нему не относится).
- **silence-when-in-sync:** терминальная (= полностью синхронизированная) задача → тишина (инвариант ORCH-068 AC-1/AC-2, теперь и для F-1).
- **Легитимный unblock сохраняется:** не-терминальная реально застрявшая задача с зелёным гейтом по-прежнему `advance` + уведомление (AC-4).
- **Наблюдаемость ORCH-068:** `skipped_terminal_total` инкрементируется при терминал-скипе F-1; `deduped_total` — при подавлении повтора dedup'ом; `unblocked_total`/`last_unblocked` — только при реальной отправке. Снимок `status()` (стр.516528) и блок `reconcile` в `GET /queue` — без структурных изменений.
- **Условность мультипроекта:** терминал-детект работает и для enduro, и для orchestrator (по группе статуса + fallback). Пайплайн/статусы enduro не трогаются.
## 4. Изменения API
Нет. HTTP-эндпоинты не меняются. `GET /queue` блок `reconcile` сохраняет форму (значения счётчиков — наблюдаемое поведение).
## 5. Изменения схемы БД
Нет. Миграции нет. (Терминальность Plane резолвится онлайн, как в ORCH-068 / Guard 2 — Вариант A без колонки статуса в `tasks`.)
## 6. Новые/изменённые QG checks
Нет. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются.
## 7. Конфигурация
Новые флаги НЕ вводятся. Терминал-скип безусловен (как у F-2). Существующие `reconcile_enabled`, `reconcile_notify_unblock`, `reconcile_skip_blocked_enabled`, `reconcile_plane_enabled` — без изменений семантики. (`reconcile_skip_blocked_enabled` гейтит ТОЛЬКО Guard 2; терминал-скип TR-1 ему НЕ подчиняется.)
## 8. Артефакты pipeline, подлежащие обновлению (документация = golden source)
- `docs/architecture/README.md` — раздел «Reconciler … ORCH-068»: дописать, что терминал-исключение и dedup теперь покрывают и F-1 (gate-side), не только F-2.
- `CHANGELOG.md` — запись `fix:` про ORCH-086.
- `docs/work-items/ORCH-086/06-adr/ADR-NNN-*.md` — ADR (создаёт архитектор).
- (Опционально) краткая ссылка в ADR ORCH-068, что F-1-пробел закрыт ORCH-086.
## 9. Готовность к development (Definition of Ready)
- G1 подтверждён по prod-логам/БД (точная стадия ET-002 и путь срабатывания задокументированы в ADR/12-review).
- Тест-план `04-test-plan.yaml` реализован в `tests/test_reconciler.py`.
- `pytest tests/ -q` зелёный.

View File

@@ -0,0 +1,32 @@
# 03-Acceptance Criteria — ORCH-086
Каждый критерий формулирует чёткое условие PASS/FAIL. Проверяется автотестами (`tests/test_reconciler.py`) и код-ревью.
## AC-1 — ET-002 (терминальная) больше не генерирует «разблокирована»
**Дано:** F-1 (`reconcile_gate_once`) обрабатывает задачу enduro, чья стадия в БД оркестратора НЕ-`done` (дрейф), а текущий статус в Plane — терминальный (`Done`, группа `completed`); гейт стадии зелёный (для enduro — no-op `True`).
- **PASS:** `_note_unblock` НЕ вызывается → `send_telegram` НЕ вызывается ни при обычном тике, ни при первом проходе после старта (свежий процесс/чистый `_unblock_dedup`).
- **FAIL:** уведомление «… разблокирована (потерян webhook)» отправлено хотя бы раз.
## AC-2 — терминальные задачи (done/cancelled) не доходят до `_note_unblock`
**Дано:** задача терминальна в Plane (группа `completed` или `cancelled`) ИЛИ её стадия в БД ∈ `{done, cancelled}`.
- **PASS:** F-1 делает ранний скип (нет `advance` / нет `_note_unblock`); `skipped_terminal_total` увеличен на 1 на каждую такую задачу за тик.
- **FAIL:** терминальная задача доходит до `advance_if_gate_passed``_note_unblock`, либо `skipped_terminal_total` не растёт.
- **Грабли (R1):** условие должно срабатывать для ОБОИХ проектов — enduro (терминал по группе `completed`/`cancelled`, либо fallback-ключ `done`/`cancelled`) и orchestrator (свой набор статусов). Тест покрывает оба пути терминал-детекта: (а) по группе, (б) fallback по логическому ключу при пустых `groups`.
## AC-3 — `_note_unblock` на всех путях получает `state_uuid` → dedup покрывает все вызовы
**Дано:** легитимный unblock реально застрявшей НЕ-терминальной задачи на пути F-1 (гейт зелёный, стадия сменилась).
- **PASS:** `_note_unblock` вызван с непустым `state_uuid`; повторный вызов для того же `issue_id`+`state_uuid` (например на следующем тике до фактической смены статуса) подавляется dedup-guard'ом → `deduped_total` растёт, второго `send_telegram` нет.
- **FAIL:** F-1 зовёт `_note_unblock` без `state_uuid` (2 аргумента) → dedup не работает → повторные уведомления.
## AC-4 — легитимный unblock реально застрявшей задачи ПО-ПРЕЖНЕМУ уведомляет (анти-регресс)
**Дано:** НЕ-терминальная задача (Plane-статус рабочий, не `done`/`cancelled`/`blocked`/`needs_input`), реально застрявшая (прошла grace, нет active-job), гейт зелёный → F-1 её продвигает (`result.advanced=True`).
- **PASS:** `_note_unblock` вызван ОДИН раз; при `reconcile_notify_unblock=True` отправлен ровно один Telegram; `unblocked_total` += 1.
- **FAIL:** уведомление подавлено (полезный алерт задушен) ИЛИ отправлено более одного раза за одну смену стадии.
## AC-5 — pytest зелёный; never-raise в тике сохранён
- **PASS:** `pytest tests/ -q` зелёный; при исключении внутри терминал-детекта/`fetch_issue_state`/`_reconcile_gate_task` тик НЕ падает (изоляция per-task), и ложное уведомление при ошибке НЕ отправляется (консервативно).
- **FAIL:** падение тика, незелёный pytest, либо исключение терминал-детекта приводит к ложной отправке.
## AC-6 — без регрессий смежного поведения (контрактный)
- **PASS:** F-2 (plane-side) терминал-скип/dedup/счётчики работают как в ORCH-068; `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, сигнатуры `advance_stage`/`advance_if_gate_passed`/`_note_unblock`, форма `status()`/`GET /queue` — не изменены; новые config-флаги не введены; `reconcile_skip_blocked_enabled` по-прежнему гейтит только Guard 2 (терминал-скип ему не подчинён). Документация (`README.md`, `CHANGELOG.md`) обновлена в том же PR.
- **FAIL:** любое из перечисленного нарушено.

View File

@@ -0,0 +1,110 @@
work_item: ORCH-086
description: >
Терминал-скип и проброс state_uuid на пути F-1 реконсилятора.
Тесты добавляются в tests/test_reconciler.py (рядом с существующими TC-01..TC-21),
переиспользуя фикстуры fresh_db / silence_side_effects / _green_ci /
plane_state_not_blocked и спай send_telegram. Все тесты — pytest, оффлайн
(Plane/Telegram мокаются), детерминированные.
tests:
- id: TC-86-01
type: unit
description: >
AC-1 — задача enduro НЕ-done в БД, но терминальная в Plane (group=completed),
гейт зелёный: F-1 НЕ вызывает _note_unblock и НЕ шлёт Telegram (ни при тике,
ни на первом проходе свежего Reconciler).
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-02
type: unit
description: >
AC-2 — терминал-скип инкрементирует skipped_terminal_total и НЕ вызывает
advance_if_gate_passed для терминальной задачи (advance_stage-спай не дёрнут).
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-03
type: unit
description: >
AC-2/R1 — терминал-детект по ГРУППЕ статуса Plane (completed/cancelled)
срабатывает независимо от проекта (enduro и orchestrator): задача в группе
cancelled тоже скипается.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-04
type: unit
description: >
AC-2/R1 — fallback терминал-детекта при пустых groups: терминальность по
логическому ключу done/cancelled проекта. Пустой groups + state_uuid ==
states['done'] -> скип.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-05
type: unit
description: >
AC-2 — терминальность по стадии БД оркестратора: задача со stage='cancelled'
(не отсекается get_active_tasks_for_reconcile, которое фильтрует только 'done')
скипается, не доходит до _note_unblock.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-06
type: unit
description: >
AC-3 — F-1 вызывает _note_unblock С непустым state_uuid (3 аргумента) на
легитимном unblock; проверяется, что dedup сохраняет ключ issue_id->state_uuid.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-07
type: unit
description: >
AC-3 — повторный F-1-тик для того же issue+state_uuid подавляется dedup-guard:
deduped_total += 1, второго send_telegram нет.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-08
type: unit
description: >
AC-4 (анти-регресс) — НЕ-терминальная реально застрявшая задача (рабочий
Plane-статус, прошла grace, нет active-job, гейт зелёный) ПО-ПРЕЖНЕМУ
продвигается и шлёт РОВНО один Telegram; unblocked_total += 1.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-09
type: unit
description: >
AC-5 — never-raise: исключение в терминал-детекте / fetch_issue_state не
роняет тик (reconcile_gate_once завершается) и НЕ приводит к ложной отправке
Telegram (консервативно: при неопределённости терминальности не уведомляем).
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-10
type: unit
description: >
AC-6 — регресс F-2: существующие TC F-2 (терминал-скип/dedup/счётчики
ORCH-068) остаются зелёными; форма status()/GET-queue не изменилась
(skipped_terminal_total, deduped_total, unblocked_total присутствуют).
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-86-11
type: unit
description: >
AC-6 — reconcile_skip_blocked_enabled=False (escape hatch Guard 2) НЕ
отключает терминал-скип TR-1: терминальная задача всё равно скипается.
module: tests/test_reconciler.py
expected: PASS
- id: TC-86-12
type: unit
description: >
Полный прогон регрессии пакета reconciler: pytest tests/test_reconciler.py
tests/test_reconciler_plane.py tests/test_config.py -q зелёный.
module: tests/test_reconciler.py
expected: PASS

View File

@@ -0,0 +1,197 @@
# ADR-001: Терминал-скип и `state_uuid`-dedup на пути F-1 реконсилятора (одиночный fetch)
## Статус
Accepted
Связано: продолжение **ORCH-068** (терминал-исключение + dedup для F-2), наследует контракты
**ORCH-053** (`adr-0007-reconciler.md`), **ORCH-060** (Guard 1/Guard 2), **ORCH-066** (статусная
модель Plane). Не вводит сквозного решения — точечный фикс существующего компонента
`src/reconciler.py`; глобальный `adr-00NN` НЕ заводится (см. §«Область и масштаб»).
## Контекст
В Telegram периодически (особенно сразу после рестарта орка) прилетает ложное
`🔧 reconciler: ET-002 done разблокирована (потерян webhook)`. Задача `ET-002`
(enduro-trails) давно завершена; реально ничего не разблокируется — это шум, вводящий
наблюдателя в заблуждение.
ORCH-068 закрыл аналогичный livelock **только на F-2 (plane-side)** двумя механизмами:
1. `_is_terminal_state(state_uuid, states, groups)` — терминал-исключение по **группе статуса
Plane** (`completed`/`cancelled`, project-independent) с fallback на логические ключи
`done`/`cancelled`. Вызывается **только** из `_reconcile_plane_issue` (F-2, `reconciler.py:362`).
2. In-memory dedup-guard `_unblock_dedup` (`issue_id → state_uuid`) внутри `_note_unblock`
(`reconciler.py:459`), активный **только когда `state_uuid is not None`**.
Оба механизма **не покрывают путь F-1 (gate-side)**. Код-аудит (golden source — текущий
`src/reconciler.py`) подтверждает две независимые причины:
- **Причина A — dedup не срабатывает.** Вызов F-1 (`_reconcile_gate_task`, `reconciler.py:228`)
передаёт `_note_unblock(work_item_id, stage)`**только 2 аргумента, без `state_uuid`**. Ветка
dedup (`reconciler.py:459463`) пропускается → уведомление шлётся на каждом релевантном тике, а
после рестарта `_unblock_dedup` пуст → первый проход снова шлёт.
- **Причина B — нет терминал-скипа.** Единственный «терминал-фильтр» F-1 —
`get_active_tasks_for_reconcile()` (`db.py`, `WHERE stage != 'done'`), который смотрит **только
на стадию задачи в БД орка** и не знает о статусе issue в Plane. Для enduro (не self-hosting)
условные гейты (`check_staging_status`/`check_deploy_status`/merge-gate/…) — no-op `(True, …)`
(условность ORCH-35/43/58/71). Поэтому задача, чья стадия в БД орка ∈ не-`done` (дрейф), но в
Plane уже `Done` (группа `completed`), проходит фильтр → `advance_if_gate_passed` находит гейт
зелёным (no-op) → `result.advanced=True` (`reconciler.py:227`) → доходит до `_note_unblock`.
Guard 2 (`_is_blocked_or_needs_input`) её не спасает: его `skip_set` = `{blocked, needs_input,
extra_waits}` и **не содержит `done`/`cancelled`**.
> **G1 (открытый вопрос BRD):** точная стадия `ET-002` в БД орка в момент срабатывания подлежит
> подтверждению в development по prod-логам/БД. Настоящее решение **робастно независимо** от точной
> стадии: терминальность определяется по группе статуса Plane (как F-2), а не по строковому
> совпадению стадии. Документирование точной стадии — в `12-review.md` (DoR TRZ §9).
## Решение
Распространить **оба** механизма ORCH-068 на путь F-1, переиспользовав один сетевой вызов на
задачу за тик. Все изменения локализованы в `src/reconciler.py` (`_reconcile_gate_task` + один
новый helper). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, сигнатуры `advance_stage`/
`advance_if_gate_passed`/`_note_unblock`, форма `status()`/`GET /queue`**не меняются**. Новых
config-флагов нет.
### D1 — Одиночный резолв Plane-статуса задачи (TR-3, R4)
Ввести приватный helper, например:
```python
def _resolve_issue_status(self, task: dict) -> tuple[dict, dict, str | None]:
"""One networked resolve per task per tick: (states, groups, current_state_uuid).
never-raise; on any failure / unresolved project / missing state ->
(states_or_{}, groups_or_{}, None). The single fetch feeds the terminal-skip
(D2), Guard 2 (D3) and the state_uuid handed to _note_unblock (D4).
"""
```
Внутри — **один** `fetch_issue_state(issue_id, pid)` плюс кэшируемые (ORCH-068 TTL)
`get_project_states(pid)` / `get_project_state_groups(pid)`. Это устраняет удвоение сетевого вызова
(сегодня `_is_blocked_or_needs_input` делает свой `fetch_issue_state` и **выбрасывает** uuid).
### D2 — Терминал-скип на F-1 (TR-1, G2), безусловный
В `_reconcile_gate_task`, **после** дешёвых локальных гардов (active-job, grace, Guard 1
retry-count — все без сети) и **до** Guard 2 / `advance_if_gate_passed`, вставить ранний guard:
```python
states, groups, state_uuid = self._resolve_issue_status(task)
# DB-side drift: cancelled is not filtered by get_active_tasks_for_reconcile (only done is).
if stage in ("done", "cancelled") or self._is_terminal_state(state_uuid, states, groups):
self.skipped_terminal_total += 1
return
```
- Терминальность — тот же `_is_terminal_state` (переиспользование, **не** дублирование): первичный
дискриминатор — группа Plane ∈ `{completed, cancelled}`; fallback при пустых `groups` — логические
ключи `done`/`cancelled`. Покрывает R1 (enduro и orchestrator с разными наборами статусов).
- Дополнительно терминальной считается задача, чья **стадия в БД**`{done, cancelled}` (дрейф
Plane↔БД; `cancelled` сейчас не отсекается на уровне выборки).
- **Безусловный** — не подчинён `reconcile_skip_blocked_enabled` (тот гейтит **только** Guard 2).
Это не маскирует легитимный replay: реально застрявшая задача терминальной в Plane не бывает.
- Инкремент `skipped_terminal_total` — единая семантика с F-2 (`reconciler.py:363`).
### D3 — Guard 2 переиспользует резолв (рефактор, без смены контракта)
`_is_blocked_or_needs_input` принимает уже резолвнутые `(states, state_uuid)` вместо собственного
`fetch_issue_state`. Поведение и kill-switch `reconcile_skip_blocked_enabled` сохранены 1:1
(флаг off → ранний `return False` без использования резолва; ошибка/`state_uuid is None`
консервативный `return True` — skip). Допустима форма с дефолтными параметрами для обратной
совместимости вызова, но единственный продакшен-вызов — из `_reconcile_gate_task` с общим резолвом.
### D4 — Проброс `state_uuid` в `_note_unblock` (TR-2, G3)
Вызов на `reconciler.py:228` передаёт третий аргумент:
```python
self._note_unblock(task.get("work_item_id") or str(task_id), stage, state_uuid)
```
`state_uuid` — тот же, что резолвнут в D1. Сигнатура `_note_unblock` **не меняется** (3-й параметр
уже опциональный). Теперь in-memory dedup (`reconciler.py:459463`) работает и на F-1:
повторный вызов для того же `issue_id`+`state_uuid` (следующий тик до фактической смены статуса) →
`deduped_total += 1`, второго Telegram нет. Если Plane недоступен и `state_uuid` достоверно
получить нельзя → `None` (dedup деградирует в no-op, как сегодня) — но первым отрабатывает
терминал-скип D2 и/или консервативный Guard 2 D3.
### Порядок гардов в `_reconcile_gate_task` (итог)
```
analysis-skip → qg-none-skip → active-job-skip → grace-skip
→ Guard 1 (retry-count, local SQL, no network)
→ [D1] resolve (states, groups, state_uuid) # единственный сетевой fetch
→ [D2] terminal-skip (unconditional) # skipped_terminal_total++
→ Guard 2 (_is_blocked_or_needs_input, reuse) # gated by reconcile_skip_blocked_enabled
→ Guard 3 (task_deps)
→ advance_if_gate_passed → [D4] _note_unblock(..., state_uuid)
```
Терминал-скип **до** Guard 2, чтобы терминальные задачи корректно увеличивали
`skipped_terminal_total` (а не молчаливо проглатывались консервативным Guard 2). Резолв D1 — после
дешёвых локальных гардов, чтобы busy/молодые задачи не порождали сетевых вызовов.
### Семантика ошибок (never-raise, R3, AC-5)
- `_resolve_issue_status` never-raise → при сбое `state_uuid=None`, `groups={}`.
- `state_uuid=None``_is_terminal_state` возвращает `False` (нельзя подтвердить терминал по
Plane), но DB-side `stage ∈ {done, cancelled}` всё ещё ловит дрейф.
- При дефолтной конфигурации (`reconcile_skip_blocked_enabled=True`) недостижимый Plane →
Guard 2 консервативно `True`**skip**, ложное уведомление не уходит (AC-5).
- Любое исключение в резолве/детекте изолировано `try/except` уровня
`reconcile_gate_once` (`reconciler.py:162168`) → тик не падает.
## Последствия
### Плюсы
- Устраняется периодический ложный «ET-002 … разблокирована»; наблюдаемо ростом
`skipped_terminal_total` в `GET /queue` (метрика успеха BRD §7).
- Робастно для обоих проектов: первичный дискриминатор — группа статуса Plane (R1).
- Один сетевой вызов на задачу за тик (не растёт нагрузка горячего цикла, R4) — резолв заодно
питает Guard 2, ранее делавший отдельный fetch.
- Dedup-страховка теперь покрывает F-1: даже если терминал-скип однажды не сработает, повтор
подавляется (`deduped_total`).
- Симметрия F-1 ↔ F-2: единая семантика терминал-исключения и счётчиков; легче сопровождать.
- Нулевой контрактный след: ни стадий, ни QG, ни схемы БД, ни новых флагов, ни смены сигнатур.
### Минусы / ограничения
- **Доп. fetch при `reconcile_skip_blocked_enabled=False`.** Раньше при выключенном Guard 2 F-1 не
ходил в Plane вовсе. Теперь терминал-скип (безусловный, по требованию TR-1) делает резолв даже
при выключенном escape-hatch. Вызов never-raise и быстро деградирует в `None`, но это новая
сетевая операция в этом режиме. **Принято** как цена корректности (TRZ §7 явно: терминал-скип не
подчинён этому флагу).
- **Угол «escape-hatch off + Plane недоступен».** При `reconcile_skip_blocked_enabled=False` И
недостижимом Plane Guard 2 не защищает, терминал-скип не подтверждает терминал (`state_uuid=None`),
и не-`cancelled` дрейф-задача может быть продвинута + уведомлена с `state_uuid=None`. Это **тот же
деградированный режим, что и сегодня** (новой гарантии под выключенный escape-hatch не даётся;
и регрессии нет). Дефолтная конфигурация полностью консервативна.
- Терминал-скип считает `skipped_terminal_total` только для задач, прошедших active-job/grace гарды
(как и F-2 считает только среди actionable issue). Это намеренно — счётчик отражает «дошло бы до
ложного unblock, но подавлено», а не «всего терминальных в системе».
### Анти-регресс (AC-4)
Легитимный unblock реально застрявшей **не-терминальной** задачи (рабочий Plane-статус, гейт
зелёный, стадия реально сменилась) по-прежнему уведомляет ровно один раз с непустым `state_uuid`
(`unblocked_total += 1`). Терминал-скип к нему не применяется (такая задача не терминальна), Guard 2
её не глушит (статус рабочий). F-2 не затронут.
## Область и масштаб (почему нет глобального ADR)
Изменение **не сквозное**: не вводит новой стадии, QG, компонента или среды; это точечное
расширение уже существующего поведения реконсилятора (ORCH-053/`adr-0007`, доработка ORCH-068).
По конвенции глобальные `adr-00NN` заводятся для сквозных решений — здесь достаточно per-work-item
ADR + обновления раздела «Reconciler» в `docs/architecture/README.md` (golden source) и
`CHANGELOG.md`. Лейбл `arch:major-change` НЕ выставляется.
## Альтернативы (отклонены)
- **Глобально выключить `reconcile_notify_unblock`** — теряем полезные алерты о реально застрявших
задачах (BRD не-цель). Подавление должно быть точечным (только терминальные).
- **Сужать выборку `get_active_tasks_for_reconcile` по статусу Plane** — потребовало бы сети в SQL-
выборке горячего цикла очереди всех проектов (анти-паттерн ORCH-026: claim/sweep offline-устойчивы)
и/или колонку статуса в `tasks` (миграция БД). Отклонено: терминальность резолвится онлайн
per-task (Вариант A, как ORCH-068 / Guard 2).
- **Только проброс `state_uuid` (D4) без терминал-скипа (D2)** — dedup подавил бы повтор в пределах
жизни процесса, но после рестарта (`_unblock_dedup` пуст) первый проход снова бы слал ложное
уведомление (ровно симптом BRD «особенно после рестарта»). Нужны оба механизма.
- **Терминал-детект по строке стадии** — хрупко при дрейфе Plane↔БД и мультипроектности (R1).
Группа статуса Plane — устойчивый дискриминатор.

View File

@@ -0,0 +1,21 @@
# 10-Tech Risks — ORCH-086
Технические риски выбранного решения (ADR-001). Бизнес-риски R1R5 — в `01-brd.md`; здесь —
реализационные риски конкретного дизайна (одиночный fetch + терминал-скип на F-1).
| # | Риск | Вероятность / Влияние | Митигация (как проверяется) |
|---|------|----------------------|------------------------------|
| TR-A | **Регрессия Guard 2 при рефакторе.** Перевод `_is_blocked_or_needs_input` на внешний резолв `(states, state_uuid)` может незаметно изменить семантику kill-switch `reconcile_skip_blocked_enabled` или консервативный fallback (`return True` при ошибке). | Низкая / Высокая | Поведение флага и fallback сохранить 1:1; контрактный тест AC-6 + регресс-тест Guard 2 (flag off → `False`; ошибка/`state_uuid=None``True`). |
| TR-B | **Угол «escape-hatch off + Plane недоступен».** При `reconcile_skip_blocked_enabled=False` и недостижимом Plane не-`cancelled` дрейф-задача может быть продвинута + ложно уведомлена (`state_uuid=None`). | Низкая / Средняя | Принятый деградированный режим (== сегодняшнее поведение, без новой гарантии). Дефолт (`flag=True`) полностью консервативен — основной тест AC-5 идёт под дефолтом. Задокументировано в ADR «Минусы». |
| TR-C | **Двойной сетевой вызов на тик.** Если резолв D1 и Guard 2 случайно оба сделают `fetch_issue_state`, нагрузка горячего цикла вырастет (R4). | Средняя / Средняя | Ровно один `fetch_issue_state` на задачу за тик; тест считает число вызовов `fetch_issue_state` (mock call_count == 1) на пути F-1. |
| TR-D | **Счётчик `skipped_terminal_total` расходится с семантикой F-2.** Двойной инкремент или инкремент не на ту задачу ломает наблюдаемость ORCH-068 (R2). | Низкая / Средняя | Инкремент ровно один раз на терминальную задачу за тик, перед `return`; тест AC-2 проверяет `+1` на задачу и отсутствие `advance`/`_note_unblock`. |
| TR-E | **Терминал-детект ломается на пустых `groups` (fallback).** При недоступности `get_project_state_groups` (пустой dict) `_is_terminal_state` должен корректно падать на логические ключи `done`/`cancelled`, иначе терминал enduro не распознается. | Низкая / Высокая | Переиспользуется существующий `_is_terminal_state` (уже покрыт для F-2); тест AC-2 покрывает обе ветви — (а) по группе, (б) fallback по ключу при пустых `groups`. |
| TR-F | **Порядок гардов.** Если терминал-скип поставить после Guard 2, терминальная задача молча проглатывается консервативным Guard 2 и `skipped_terminal_total` не растёт (теряем метрику успеха). | Низкая / Средняя | Терминал-скип строго ДО Guard 2 (ADR порядок гардов); тест проверяет инкремент счётчика именно при терминале. |
| TR-G | **never-raise в новом helper.** Исключение в `_resolve_issue_status`/`_is_terminal_state` не должно ронять тик и не должно приводить к ложной отправке. | Низкая / Высокая | helper под `try/except``(…, None)`; тик уже изолирован `reconcile_gate_once` (`reconciler.py:162`). Тест AC-5: исключение в fetch → тик жив, `send_telegram` не вызван. |
| TR-H | **Анти-регресс легитимного unblock (AC-4).** Слишком широкий терминал/skip-set может задушить полезный алерт о реально застрявшей не-терминальной задаче. | Низкая / Высокая | Терминал-детект строго по `{completed, cancelled}` (+ DB `done`/`cancelled`); регресс-тест AC-4 — не-терминальная задача с зелёным гейтом уведомляет ровно один раз. |
## Зависимости / предпосылки
- `fetch_issue_state`, `get_project_states`, `get_project_state_groups`, `get_project_by_repo`
переиспользуются read-only, без изменения контракта (TRZ §1).
- G1 (точная стадия `ET-002`) подтверждается в development по prod-логам/БД и фиксируется в
`12-review.md` (DoR TRZ §9). Решение робастно независимо от исхода G1.

View File

@@ -0,0 +1,51 @@
---
type: review
work_item_id: ORCH-086
verdict: APPROVED
version: 1
---
# Review ORCH-086
## Summary
Терминал-скип и `state_uuid`-dedup распространены на путь F-1 реконсилятора, закрывая F-1-пробел
ORCH-068 (ложное «ET-002 done разблокирована (потерян webhook)»). Изменение полностью локализовано
в `src/reconciler.py` (новый `_resolve_issue_status` + врезка ранних гардов в `_reconcile_gate_task`
+ переиспользование резолва в `_is_blocked_or_needs_input` через опц. аргументы с `_UNSET`-sentinel
для обратной совместимости). Реализация 1:1 соответствует ТЗ (TR-1/TR-2/TR-3) и ADR-001 (D1D4).
`STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, сигнатуры `advance_stage`/`advance_if_gate_passed`/
`_note_unblock`, форма `status()`/`GET /queue`, config-флаги — без изменений. Контракт never-raise
сохранён на всех новых путях. Полный прогон `pytest tests/ -q` зелёный — 1069 passed.
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
## Документация
Обновлена в том же PR, соответствует требованию «golden source наравне с кодом» (CLAUDE.md §2,
TRZ §8):
- `docs/architecture/README.md` — раздел Reconciler F-1 дополнен блоком ORCH-086 (терминал-скип +
dedup на F-1, единый fetch на тик, безусловность относительно `reconcile_skip_blocked_enabled`).
- `CHANGELOG.md` — запись `fix:` ORCH-086 с описанием корня (причины A/B) и фикса (D1D4).
- `docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md` — присутствует,
Accepted, описывает решение, порядок гардов, семантику ошибок и отклонённые альтернативы.
- API не менялось → обновление таблицы API не требуется. Per-work-item ADR достаточно (точечный фикс
существующего компонента, не сквозное решение — обосновано в §«Область и масштаб»).
## Контроль качества
- Тесты содержательные, не тривиальные: TC-86-01..09/11 (`tests/test_reconciler.py`) покрывают
терминал по группе `completed`/`cancelled`, fallback по логическому ключу при пустых `groups`,
DB-side `cancelled` без обращения к Plane, проброс/dedup `state_uuid`, анти-регресс легитимного
unblock, never-raise без ложного уведомления, независимость терминал-скипа от Guard-2-флага;
TC-86-10 (`tests/test_reconciler_plane.py`) — неизменность формы `status()`; TC-86-12 — зелёный
регресс-прогон. Сопутствующая правка `tests/test_orch026_task_deps.py` корректно адаптирует мок
Guard 2 под новую сигнатуру и держит резолв offline.
- `task.get("plane_id") or task.get("plane_issue_id")` в `_resolve_issue_status` — дословный перенос
ранее протестированной логики Guard 2 (ORCH-060), регрессии нет.

View File

@@ -0,0 +1,67 @@
---
type: test-report
work_item_id: ORCH-086
result: PASS
---
# Test Report — ORCH-086
Терминал-скип и проброс/dedup `state_uuid` на пути F-1 реконсилятора (закрытие F-1-пробела
ORCH-068: ложное «ET-002 done разблокирована (потерян webhook)»).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Repo / ветка: orchestrator @ `feature/ORCH-086-orch-86-reconciler-telegram-et` (worktree)
- Prod health (8500): `{"status":"ok","service":"orchestrator"}` — OK
- Дата: 2026-06-09
## Предусловия
- Review-вердикт (`12-review.md`): **APPROVED** (P0/P1/P2 — нет).
## Результаты
| TC ID | Описание | Тест | Результат |
|-------|----------|------|-----------|
| TC-86-01 | AC-1 — терминальная enduro-задача (group=completed), зелёный гейт: нет `_note_unblock`/Telegram | `test_tc86_01_terminal_in_plane_not_unblocked` | PASS |
| TC-86-02 | AC-2 — терминал-скип `++skipped_terminal_total`, нет `advance_if_gate_passed` | `test_tc86_02_terminal_skip_counter_no_advance` | PASS |
| TC-86-03 | AC-2/R1 — терминал по ГРУППЕ (cancelled), независимо от проекта | `test_tc86_03_terminal_by_group_cancelled` | PASS |
| TC-86-04 | AC-2/R1 — fallback по логическому ключу done/cancelled при пустых groups | `test_tc86_04_terminal_fallback_logical_key_empty_groups` | PASS |
| TC-86-05 | AC-2 — терминальность по стадии БД (`stage='cancelled'`) | `test_tc86_05_terminal_by_db_stage_cancelled` | PASS |
| TC-86-06 | AC-3 — легитимный unblock зовёт `_note_unblock` с непустым `state_uuid` | `test_tc86_06_legit_unblock_passes_state_uuid` | PASS |
| TC-86-07 | AC-3 — повторный тик для того же issue+state_uuid подавлен dedup (`++deduped_total`) | `test_tc86_07_repeat_tick_deduped` | PASS |
| TC-86-08 | AC-4 (анти-регресс) — реально застрявшая задача продвигается, ровно один Telegram, `++unblocked_total` | `test_tc86_08_legit_unblock_still_notifies` | PASS |
| TC-86-09 | AC-5 — never-raise: исключение в детекте не роняет тик и не шлёт ложного Telegram | `test_tc86_09_never_raise_no_false_notify` | PASS |
| TC-86-10 | AC-6 — форма `status()`/`GET /queue` неизменна (счётчики на месте) | `test_tc86_10_status_shape_unchanged` (test_reconciler_plane.py) | PASS |
| TC-86-11 | AC-6 — `reconcile_skip_blocked_enabled=False` НЕ отключает терминал-скип | `test_tc86_11_terminal_skip_independent_of_guard2_flag` | PASS |
| TC-86-12 | Полный регресс пакета reconciler/config зелёный | `pytest tests/test_reconciler.py tests/test_reconciler_plane.py tests/test_config.py` | PASS |
## Smoke test API (prod 8500)
- `GET /health``{"status":"ok","service":"orchestrator"}` — OK
- `GET /status` → 200, валидный JSON (`active_tasks` присутствует) — OK
- `GET /queue` → 200, блок `reconcile` присутствует (`enabled`, `unblocked_total`, `last_unblocked`, `interval`) — OK
## Вывод pytest
Полный прогон:
```
1069 passed, 1 warning in 26.16s
```
Целевой регресс-пакет (TC-86-12):
```
78 passed, 1 warning in 2.38s
```
(единственный warning — PydanticDeprecatedSince20 в `src/config.py:5`, не связан с задачей.)
## Покрытие критериев приёмки
- AC-1 — TC-86-01 ✓
- AC-2 — TC-86-02/03/04/05 ✓
- AC-3 — TC-86-06/07 ✓
- AC-4 — TC-86-08 ✓
- AC-5 — TC-86-09 + зелёный полный прогон ✓
- AC-6 — TC-86-10/11 + контракты (STAGE_TRANSITIONS/QG_CHECKS/схема БД/сигнатуры не тронуты) ✓
## Итог
**PASS** — все 12 тест-кейсов PASS, полный регресс `pytest tests/` зелёный (1069 passed),
smoke API OK. Задача готова к переходу на стадию `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-086
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,31 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T23:25:53Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live staging stand (`orchestrator-staging`, port 8501).
Canonical run inside the container (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**.
- REAL failed: none
- All REAL checks green: A1, A2, A3 (SMOKE), B4, B5, B6 (ACCESS), C7, C8 (E2E).
- SANDBOX_INFRA failed (waived per ORCH-061): C9a, C9b — known sandbox-infra checks
that 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
```
Staging gate PASSED → advance.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-87: трекер-карточка застревает на старом статусе (To Analyse) + осиротевшие карточки при bump
Work Item ID: ORCH-087
## Description
TBD

View File

@@ -0,0 +1,69 @@
# BRD — ORCH-087
**Тип:** Багфикс (UX live-трекера) + малая фича (эффорт в карточке) + корректность метрики времени
**Приоритет:** MEDIUM
**Зона:** `src/notifications.py` (`update_task_tracker` bump-режим, `render_task_tracker`), `src/db.py` (учёт message_id / колонка effort), `src/agents/launcher.py` + `src/usage.py` (стамп эффорта)
**Связь:** ORCH-067 (формат карточки/ссылки/статусы), ORCH-042 (режим bump), ORCH-52h/ORCH-081 (эффорт реально работает), ORCH-086 (свежий reconciler — см. G6)
---
## 1. Контекст и проблема
Каждая задача имеет ОДНУ live-карточку в Telegram (`update_task_tracker`, инвариант «одна карточка на задачу»). Дефолтный режим — `bump` (ORCH-067): на каждом обновлении старая карточка удаляется и новая шлётся вниз чата, указатель `tasks.tracker_message_id` перепонтуется на свежий `message_id`.
**Скриншот Славы (08.06, задача ORCH-082):**
1. В чате висит карточка с заголовком `📍 To Analyse`, хотя конвейер прошёл весь путь и все стадии ✅ вплоть до «Внедрение».
2. Статусы деплоя не отражены (нет `⏸️ Awaiting Deploy / Confirm Deploy`), хотя задача реально на стадии `deploy`.
**Диагноз код-аудита (08.06):** сам рендер `render_task_tracker` исправен (на стадии `deploy` корректно даёт заголовок и весь deploy-цикл). Карточка со скриншота — **ОСИРОТЕВШАЯ** старая (`msg 18204`), застрявшая на первом рендере (`To Analyse` = `_DEFAULT_STATUS_LABEL`). `bump` её не удалил: `delete_telegram(mid)` — best-effort и НЕ блокирует `send` (BR-6); указатель `tracker_message_id` хранит ТОЛЬКО последний `mid`, поэтому удаляется только он. При рассинхроне указателя часть карточек осиротевает и висит «замёрзшей» на старом статусе. Проверено: бот МОЖЕТ удалять (`deleteMessage → ok:true` и для 18204, и для 18227) — дело не в правах, а в **потере ссылки на старые `message_id`**.
**Расширение (09.06) — G5:** итоговое время в карточке (`⏱️ Всего … · агенты … · твоё …`) считается неверно — раздувается на простое/застое (пример ORCH-087: «Подтверждение BRD 392м» при реальном отсутствии обдумывания).
**Расширение — эффорт в карточке:** строка стадии показывает модель (`opus-4-8`), но НЕ эффорт. После ORCH-52h эффорт реально работает (developer=`xhigh`, прочие `high`/`medium`) — его надо показать.
## 2. Цель
Обеспечить, чтобы в чате жила РОВНО ОДНА актуальная карточка задачи с корректным текущим статусом (включая весь deploy-цикл), без осиротевших «замёрзших» карточек; показать эффорт каждой стадии рядом с моделью; считать итоговое время честно и сходимо. Перед разработкой G0-исследование фиксирует ТОЧНУЮ механику рассинхрона и даёт обоснованную (data-backed) рекомендацию `bump` vs `edit` → ADR.
## 3. Бизнес-требования
| ID | Требование |
|----|-----------|
| **BR-G0** | **Сначала расследование, не фикс вслепую.** Установить точную механику bump-режима, не принимая на веру workaround-диагноз. Ответить на вопросы расследования (см. §4). Воспроизвести на staging. Вывод → ADR (`06-adr/`), и только ПОТОМ фикс. |
| **BR-G1** | Не оставлять осиротевших карточек: при bump гарантировать удаление ВСЕХ ранее созданных карточек задачи (хранить полный учёт `message_id`, а не только последний), либо иной механизм, доказательно исключающий сирот. |
| **BR-G2** | Заголовок живой карточки отражает ТЕКУЩУЮ стадию на каждой карточке — не застывает на `To Analyse`. |
| **BR-G3** | Статусы деплоя (`Awaiting Deploy` / `Deploying` / `Confirm Deploy` / `Monitoring` / `Done`) видимы на карточке на соответствующих стадиях (offline-label + live-overlay покрывают весь deploy-цикл). |
| **BR-EFF** | Строка каждой стадии карточки показывает уровень эффорта рядом с моделью (формат `… · opus-4-8 · xhigh` или `opus-4-8/xhigh`). developer-строка → `xhigh`; механические (tester/deployer) → `medium`. |
| **BR-G5** | Итоговое время разделить честно: (1) чистое рабочее время агентов (Σ `agent_runs`) — главная метрика; (2) человеческое время BRD-approve — ТОЛЬКО фактическое, без аномального застоя/рассинхрона; (3) wall-clock — если показываем, помечать как «общее (с ожиданием)», не выдавать за рабочее. Итог должен СХОДИТЬСЯ. |
| **BR-G6** | Ветка ORCH-087 должна разрабатываться/мержиться поверх свежего `origin/main` (где уже ORCH-86). Использовать свежий `notifications/reconciler` из 86. Явно проверить на merge-gate (пересечение `reconciler.py` — не append-only, `.gitattributes union` не спасёт). |
## 4. Вопросы G0-расследования (обязательны к ответу в ADR)
1. **Сколько РЕАЛЬНО карточек одной задачи висело** в чате к моменту бага (собрать `message_id` из логов/Telegram) — сирот могло быть >1.
2. **В какие МОМЕНТЫ `tracker_message_id` рассинхронизируется** с реальными сообщениями:
- (a) `send` вернул `None` (нет креды / transient) → `mid` не перезаписан;
- (b) рестарт орка между `delete` и `send`;
- (c) пересоздание карточки во время CLI-фикса / ручных операций;
- (d) гонка двух `update_task_tracker` подряд (быстрые стадии);
- (e) `delete` упал (rate-limit / >48ч), но `send` прошёл.
3. **Почему ИМЕННО заголовок застывает на `To Analyse`:** это старый рендер (до смены stage) или баг плана-лейбла? Воспроизвести на staging: прогнать задачу, на каждой стадии зафиксировать что РЕАЛЬНО в Telegram (заголовок+тело) vs что в БД (`stage`).
4. **`bump` vs `edit`:** какой режим реально надёжнее против сирот — замерить, а не предполагать. `edit` правит ОДНО сообщение in-place (нет сирот, но не держит карточку внизу); `bump` держит внизу (фича-просьба ORCH-042), но плодит сирот при рассинхроне. Дать обоснованную рекомендацию с данными.
## 5. Не-цели
- Не плодить дубликаты — инвариант «одна карточка на задачу» сохранить.
- Не пинговать — `disable_notification` остаётся (карточка тихая).
- Не ломать ссылки ORCH-067 (`plane_issue_link`, кликабельный номер) и `disable_web_page_preview` (ORCH-080).
- Не вводить новую стадию конвейера / не менять `STAGE_TRANSITIONS` / `QG_CHECKS`.
- Не предрешать `bump` vs `edit` в BRD — это вывод G0/ADR.
## 6. Ограничения и грабли
- Telegram не даёт удалять сообщения **старше 48ч** — для совсем старых сирот зачистка может не сработать. Документировать как ограничение (`delete_telegram` уже классифицирует это как «gone»/не-transient).
- Эффорт **не возвращается** Claude CLI в result-JSON (в отличие от модели, которая берётся из `modelUsage`). Поэтому надёжный источник эффорта — стамп резолва (`resolve_agent_effort`) В МОМЕНТ запуска, а не пересчёт постфактум.
- Контракт всего компонента нотификаций — **never raises**; карточка всегда silent.
- Self-hosting: задача правит инструмент, работающий в проде и обслуживающий enduro-trails. НЕ ронять прод-контейнер; обязательная страховка — `deploy-staging` (8501).
## 7. Бизнес-ценность
Наблюдатель (Слава) видит ровно одну достоверную карточку: текущий статус, эффорт каждой стадии и честное время. Уходит класс багов «замёрзшая сирота вводит в заблуждение» и «магическое раздутое итоговое время».

View File

@@ -0,0 +1,117 @@
# ТЗ — ORCH-087
Техническое задание для архитектора/разработчика. Конкретные изменения кода/БД с привязкой к BR (см. `01-brd.md`). Архитектурные РЕШЕНИЯ (выбор механизма зачистки сирот, выбор `bump`/`edit`, формула отсечки аномалий времени) принимает архитектор в ADR на основе G0 — здесь зафиксированы требования и точки врезки.
---
## 0. Задействованные модули `src/`
| Модуль | Роль в задаче |
|--------|---------------|
| `src/notifications.py` | `update_task_tracker` (bump/edit), `render_task_tracker`, `_stage_line`, итоговая строка времени, `plane_status_label`/`_card_status_label` (заголовок/deploy-цикл) |
| `src/db.py` | учёт `message_id` карточек задачи (BR-G1); колонка `agent_runs.effort` (BR-EFF); геттеры/сеттеры |
| `src/agents/launcher.py` | `_spawn`: стамп `resolve_agent_effort(agent)` в `agent_runs.effort` в момент запуска (BR-EFF) |
| `src/usage.py` | `short_model_name` (рядом — рендер эффорта); при необходимости — пробрасывать effort в строку стадии |
| `tests/test_notifications*.py`, `tests/test_*tracker*` | unit-покрытие |
**НЕ трогать** (BR-G6): `src/reconciler.py` / `tests/test_reconciler.py` — задача не требует их правок; пересечение с ORCH-86 неприемлемо. Если правка всё же понадобится — сохранить ORCH-086 (`skipped_terminal_total`, `state_uuid`-dedup, terminal-skip F-1) и явно проверить на merge-gate.
## 1. G0 — расследование (BR-G0) → ADR
- Исследование выполняется ДО кода: собрать факты по §4 BRD (логи орка `data/runs`, Telegram message_id, БД `tracker_message_id`/`stage` по ORCH-082), воспроизвести прогон на staging (8501), зафиксировать таблицу «стадия → (заголовок+тело в Telegram) vs (stage в БД)».
- Артефакт расследования и обоснованная рекомендация `bump` vs `edit``06-adr/ADR-NNN-tracker-orphan-cleanup.md`.
- Код фикса (G1G3) реализует выбранный в ADR механизм. ТЗ ниже задаёт ИНВАРИАНТЫ, которым любой выбранный механизм обязан удовлетворять.
## 2. G1 — гарантированная зачистка сирот (BR-G1)
**Требование-инвариант:** после любого `update_task_tracker` в чате не остаётся НИ ОДНОЙ ранее созданной карточки этой задачи, кроме текущей (в пределах 48ч-лимита Telegram).
Точка проблемы (текущий код, `update_task_tracker`, ветка `mode == "bump"`):
```python
if mid is not None:
delete_telegram(mid) # удаляется ТОЛЬКО последний mid
new_mid = send_telegram(text, disable_notification=True)
if new_mid is not None:
set_tracker_message_id(task_id, new_mid)
```
`tasks.tracker_message_id` — скаляр (последний `mid`). При рассинхроне (send→None / рестарт между delete и send / пересоздание / гонка / delete-fail+send-ok) прежние карточки теряют ссылку и осиротевают.
**Требования к решению (любой механизм из ADR):**
- R-1. Система должна знать обо ВСЕХ незакрытых `message_id` карточек задачи (а не только о последнем), чтобы подчищать их при следующем bump / на рассинхроне / при старте.
- R-2. Перед/в момент создания новой карточки удаляются ВСЕ известные незакрытые `message_id`; успешно удалённые (включая «already gone» по `_DELETE_GONE_MARKERS`) исключаются из учёта; не удалённые transient — остаются в учёте для повторной попытки.
- R-3. Новый `message_id` записывается в учёт ТОЛЬКО при успешном `send` (`new_mid is not None`) — transient send не должен обнулять/терять учёт (сохранить текущую защиту BR-6).
- R-4. Инвариант «одна карточка на задачу» и «не более одного `send` за вызов» сохраняются → дубликатов внутри вызова нет.
- R-5. **Кандидатные механизмы для ADR** (выбор за архитектором, не предрешать в коде до ADR):
- (A) bump + полный учёт `message_id` (новая таблица `tracker_messages(task_id, message_id, created_at, deleted_at)` ИЛИ JSON-массив в колонке `tasks.tracker_message_ids`), зачистка всех незакрытых;
- (B) переход дефолта на `edit` (нет сирот by design; теряется «карточка внизу» ORCH-042) — взвесить против фича-просьбы.
- R-6. Изменение схемы БД (если выбран вариант A) — строго аддитивное (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), идемпотентное, restart-safe на живой общей прод-БД (данные enduro не трогаются). Детали данных — `08-data-requirements.md`.
## 3. G2 — заголовок отражает текущую стадию (BR-G2)
- Рендер `render_task_tracker` уже строит заголовок/статус-строку из `tasks.stage` (`plane_status_label``_card_status_label`). Замёрзший `To Analyse` — следствие осиротевшей карточки (G1), а не бага рендера.
- Требование: после фикса G1 единственная живая карточка всегда несёт заголовок текущей стадии. Регресс-тест: на каждой стадии заголовок/статус-строка соответствуют `stage` в БД (часть staging-воспроизведения G0 + unit на `plane_status_label`).
## 4. G3 — deploy-цикл на карточке (BR-G3)
- Проверить, что `_STAGE_STATUS_LABEL["deploy"]` (`⏸️ Awaiting Deploy — ожидание Confirm Deploy`) + live-overlay `_live_plane_branch_override` (`deploying`, `monitoring`) покрывают весь цикл `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`.
- Если какой-то под-статус не отображается на соответствующей стадии — добить offline-label/overlay. `Done` рендерится из `stage == "done"`. Контракт never-raise и kill-switch `tracker_live_status` сохраняются.
## 5. BR-EFF — эффорт в строке стадии
**API/данные:**
- Новая колонка `agent_runs.effort TEXT` (миграция `_ensure_column(conn, "agent_runs", "effort", "TEXT")` в `src/db.py`, рядом с `model`).
- **Стамп в момент запуска** (`launcher._spawn`): сразу после резолва `effort = resolve_agent_effort(agent, project_id)` записать его в строку `agent_runs` (тот же `run_id`). Источник — РЕАЛЬНО ушедшее в `--effort` значение (`""`/без флага → сохранить пусто/`NULL`). Это надёжнее пересчёта (CLI не возвращает эффорт в result-JSON).
- Допустимо: расширить `INSERT INTO agent_runs (task_id, agent, effort) VALUES (?,?,?)` или отдельным `UPDATE agent_runs SET effort=? WHERE id=?` после резолва. Выбор — архитектор; значение должно соответствовать фактическому флагу запуска.
**Рендер** (`render_task_tracker._stage_line`):
- Текущий суффикс: `f" · {model}"` при наличии модели.
- Добавить эффорт рядом: формат `· opus-4-8 · xhigh` ИЛИ компактно `· opus-4-8/xhigh` (на усмотрение, выбрать единый). При пустом эффорте — суффикс эффорта опускается (как опускается модель при пустой `short_model_name`).
- Брать `effort` из строки `agent_runs` соответствующей стадии (последний завершённый run, как `model`). Допустим fallback на `resolve_agent_effort(agent)` для исторических строк без колонки.
**Ожидаемо:** developer-строка → `xhigh`; tester/deployer → `medium`; analyst/architect/reviewer → `high` (по таблице ORCH-41/081).
## 6. BR-G5 — честное и сходимое итоговое время
Текущая итоговая строка (`done`):
```python
wall = _duration_seconds(created_at, updated_at) # раздут: вся очередь+ожидание+застой
review_seconds = _duration_seconds(brd_review_started, brd_review_ended) # раздут при застое
"⏱️ Всего {wall} · агенты {agent_seconds} · твоё {review}"
```
Проблема: `wall ≠ agent_seconds + review_seconds` (незалогированные queue-паузы) → итог визуально «врёт»; `review_seconds` засчитывает застой/рассинхрон (ORCH-087: 392м).
**Требования (формула — за архитектором, G5 «КАК — архитектору»):**
- T-1. Чистое рабочее время агентов = `Σ _duration_seconds(started, finished)` по `agent_runs` (текущий `agent_seconds`) — **главная метрика**, оставить точной.
- T-2. Человеческое BRD-время — ТОЛЬКО фактическое: НЕ включать аномальный застой/рассинхрон (`brd_review` болтался открытым из-за рассинхрона In Review→Backlog). Ограничить разумным порогом ИЛИ считать только активные окна. Аномалия не должна показываться как «твоё время».
- T-3. Wall-clock — если показываем, помечать как «общее (с ожиданием)», НЕ выдавать за рабочее время.
- T-4. Итог должен СХОДИТЬСЯ: либо `wall = Σ(стадии) + Σ(паузы с подписью)`, либо не показывать wall как сумму. Прозрачность вместо «магического» числа.
- T-5. `agent_runs`-агрегация (`total_in/total_out/total_cost/agent_seconds`) и `💰`-строка — без регресса.
## 7. Изменения API (endpoints)
Нет новых/изменённых HTTP-endpoint. (Опционально — отразить учёт карточек/effort в read-only снимке `GET /queue`, если архитектор сочтёт нужным; не обязательно.)
## 8. Изменения схемы БД
- `agent_runs.effort TEXT` — аддитивно, идемпотентно (`_ensure_column`). **Обязательно.**
- Учёт `message_id` (BR-G1, если выбран вариант A) — аддитивная таблица `tracker_messages` ИЛИ колонка-массив `tasks.tracker_message_ids`. **Зависит от ADR.** Подробности — `08-data-requirements.md`.
- Существующие колонки/таблицы (`tasks.tracker_message_id`, `brd_review_*`, `agent_runs.model`) — не ломать; при варианте A сохранить обратную совместимость со скалярным `tracker_message_id` (миграция/со-существование).
## 9. Требования к новым QG-проверкам
Нет. `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, машинные вердикты гейтов — без изменений.
## 10. Артефакты pipeline, создаваемые/обновляемые
- `06-adr/ADR-NNN-tracker-orphan-cleanup.md` (G0 вывод + рекомендация bump/edit + механизм G1 + формула G5) — архитектор.
- Обновить `CLAUDE.md` (§ Нотификации) и `docs/architecture/README.md` (компонент Notifications) — отразить учёт карточек, эффорт-в-строке, честное время. **Golden source наравне с кодом.**
- `CHANGELOG.md``## [Unreleased]` запись (под `.gitattributes merge=union`).
## 11. Инварианты (не нарушать)
- never-raise во всём пути нотификаций; карточка всегда silent (`disable_notification`).
- «одна карточка на задачу»; ≤1 `send` за вызов `update_task_tracker`.
- Ссылки ORCH-067 (`plane_issue_link`), `disable_web_page_preview` ORCH-080 — сохранены.
- `STAGE_TRANSITIONS` / `QG_CHECKS` / стадии конвейера — без изменений.
- БР-G6: разработка/merge поверх свежего `origin/main` (ORCH-86); `reconciler.py` не эродировать.
- Миграции БД аддитивны и идемпотентны (общая прод-БД, enduro не трогать).

View File

@@ -0,0 +1,71 @@
# Критерии приёмки — ORCH-087
Каждый критерий — чёткое условие PASS/FAIL. Привязка к BR (`01-brd.md`) и ТЗ (`02-trz.md`).
---
## G0 — расследование
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-0.1 | ADR `06-adr/ADR-NNN-tracker-orphan-cleanup.md` существует и отвечает на ВСЕ 4 вопроса §4 BRD (число реальных сирот; точки рассинхрона ae; причина застывания `To Analyse`; bump vs edit с данными). | ADR содержит ответы по всем 4 пунктам + явную рекомендацию | Любой вопрос без ответа / рекомендация без обоснования |
| AC-0.2 | В ADR зафиксировано staging-воспроизведение: таблица «стадия → (заголовок+тело в Telegram) vs (stage в БД)» по прогону задачи на 8501. | Таблица воспроизведения приложена | Воспроизведения нет / только предположения |
| AC-0.3 | Фикс (G1G3) реализует механизм, выбранный и обоснованный в ADR (не противоречит выводу). | Код соответствует ADR | Код расходится с ADR без объяснения |
## G1 — нет осиротевших карточек
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-1.1 (=AC-1) | После прохождения стадий в чате НЕ остаётся карточек с устаревшим заголовком (нет `To Analyse` на завершённой задаче). | На staging-прогоне в чате только одна карточка, заголовок актуальный | Видна ≥1 замёрзшая/устаревшая карточка |
| AC-1.2 | Система ведёт учёт ВСЕХ незакрытых `message_id` задачи (не только последнего); при bump удаляются ВСЕ известные незакрытые. | Учёт присутствует, unit-тест на мульти-mid зачистку зелёный | Учёт только скаляр / сироты остаются |
| AC-1.3 (=AC-3) | При сбое `send` (`new_mid=None`) / рестарте орка / гонке указатель не теряет старые карточки — они подчищаются (или остаются в учёте до следующей попытки). | Unit моделирует send→None / повторный вызов: прежние mid не потеряны | mid теряется → сирота |
| AC-1.4 | Telegram-лимит 48ч на удаление задокументирован как known-limitation (старые сироты могут не удалиться). | Ограничение в ADR/доке | Не упомянуто |
## G2 — актуальный заголовок
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-2.1 (=AC-2) | Единственная актуальная карточка показывает текущий статус, включая весь deploy-цикл. | На каждой стадии заголовок/статус соответствует `stage` в БД | Расхождение заголовка и `stage` |
| AC-2.2 | `plane_status_label(stage)` детерминированно даёт корректный лейбл для всех стадий `created…done` (unit). | Unit перебирает все стадии, лейблы верны | Любой stage даёт неверный/`To Analyse` по умолчанию некорректно |
## G3 — deploy-цикл виден
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-3.1 | Стадия `deploy` показывает `⏸️ Awaiting Deploy` (offline). | Unit/staging подтверждает | Не показывает |
| AC-3.2 | Live-overlay покрывает `Deploying` / `Monitoring` (когда Plane-статус реально такой). | Overlay рисует ветку при наличии UUID статуса | Ветка не рисуется при живом статусе |
| AC-3.3 | `Done` рендерится по `stage == "done"` (`ГОТОВО` + итог). | Карточка done корректна | — |
## BR-EFF — эффорт в карточке
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-E.1 | Колонка `agent_runs.effort` создаётся идемпотентно; стамп фактического эффорта происходит в момент запуска агента. | Миграция + стамп есть, unit подтверждает запись | Колонки нет / эффорт не стампится |
| AC-E.2 | Строка каждой завершённой стадии карточки показывает эффорт рядом с моделью (выбранный формат `· model · effort` или `· model/effort`). | Рендер содержит эффорт, unit зелёный | Эффорт отсутствует в строке |
| AC-E.3 | developer-строка показывает `xhigh`; tester/deployer — `medium`; analyst/architect/reviewer — `high`. | Значения соответствуют ORCH-41/081 | Значения не совпадают |
| AC-E.4 | Пустой/неизвестный эффорт → суффикс эффорта опускается, рендер не падает. | Unit на пустой effort зелёный | Падение/мусорный суффикс |
## BR-G5 — честное время
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-5.1 | На задаче с искусственным застоем (открытый `brd_review` ~6ч) итоговое «твоё время» НЕ показывает ~6ч. | Unit с brd-окном 6ч → «твоё время» ограничено/активное, не 6ч | Показывает ~6ч |
| AC-5.2 | agent-время = `Σ agent_runs` точно (без регресса). | Unit сверяет сумму | Расхождение |
| AC-5.3 | Числа в итоговой строке сходятся: wall помечен как «общее (с ожиданием)» ИЛИ wall = Σ(стадии)+Σ(паузы с подписью). | Итог прозрачен и согласован | wall выдаётся за рабочее/не сходится |
## BR-G6 — свежий main / без эрозии reconciler
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-6.1 | Ветка разработана/смержена поверх `origin/main`, содержащего ORCH-86 (`merge-base` = merge-коммит 86 или новее). | `git merge-base --is-ancestor origin/main HEAD` → true; маркеры ORCH-086 в `src/reconciler.py` ветки присутствуют | Ветка отстаёт / маркеры 86 потеряны |
| AC-6.2 | `src/reconciler.py` / `tests/test_reconciler.py` не эродированы (ORCH-086 terminal-skip + `state_uuid`-dedup на месте). Проверено на merge-gate. | Диф не удаляет ORCH-086 логику; merge-gate зелёный | Логика 86 затёрта |
## Сквозные
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| AC-X.1 (=AC-4) | Инвариант «одна карточка на задачу» соблюдён; дубликатов нет; ≤1 `send` за вызов. | Unit/staging: одна карточка | Дубликаты |
| AC-X.2 (=AC-5 задачи) | `pytest tests/ -q` зелёный; весь путь нотификаций never-raise (любая ошибка Telegram/БД не валит конвейер). | Тесты зелёные; unit на исключения не поднимает | Падение/raise |
| AC-X.3 | Документация обновлена в ТОМ ЖЕ PR: `CLAUDE.md` (§Нотификации), `docs/architecture/README.md` (Notifications), `CHANGELOG.md`. | Доки обновлены | Reviewer → REQUEST_CHANGES |
| AC-X.4 | Ссылки ORCH-067 (`plane_issue_link`) и `disable_web_page_preview` (ORCH-080) сохранены. | Кликабельный номер + нет link-preview | Регресс |
| AC-X.5 | `STAGE_TRANSITIONS` / `QG_CHECKS` без изменений; миграции БД аддитивны/идемпотентны (enduro-данные не тронуты). | Диф не меняет машину стадий; миграции безопасны | Изменение машины стадий / небезопасная миграция |

View File

@@ -0,0 +1,115 @@
work_item: ORCH-087
description: >
Тест-план для багфикса live-трекера (сироты/заголовок/deploy-цикл),
эффорта-в-карточке, честного итогового времени. Юнит-тесты — pytest,
изоляция Telegram через monkeypatch (send/edit/delete не ходят в сеть).
Интеграция/воспроизведение — на staging (8501). Контракт never-raise
проверяется отдельными negative-тестами.
tests:
# ---------------- G1: зачистка сирот ----------------
- id: TC-01
type: unit
description: "bump удаляет ВСЕ известные незакрытые message_id задачи, не только последний (мок delete/send)"
module: tests/test_notifications_orphans.py
expected: PASS
- id: TC-02
type: unit
description: "send вернул None (нет креды/transient) → учёт прежних message_id не теряется, mid не обнуляется (BR-6 + R-3)"
module: tests/test_notifications_orphans.py
expected: PASS
- id: TC-03
type: unit
description: "delete вернул False (transient, >48ч) → message_id остаётся в учёте для повторной попытки; 'already gone' (_DELETE_GONE_MARKERS) → исключается из учёта"
module: tests/test_notifications_orphans.py
expected: PASS
- id: TC-04
type: unit
description: "повторные вызовы update_task_tracker подряд (быстрые стадии/гонка) → ровно одна живая карточка, ≤1 send за вызов, без дублей (AC-X.1)"
module: tests/test_notifications_orphans.py
expected: PASS
- id: TC-05
type: unit
description: "учёт message_id переживает 'рестарт' (читается из БД) → старые карточки подчищаются при следующем bump (AC-1.3)"
module: tests/test_notifications_orphans.py
expected: PASS
# ---------------- G2/G3: заголовок и deploy-цикл ----------------
- id: TC-06
type: unit
description: "plane_status_label детерминированно даёт корректный лейбл для всех stage created..done; deploy → 'Awaiting Deploy' (AC-2.2, AC-3.1)"
module: tests/test_notifications.py
expected: PASS
- id: TC-07
type: unit
description: "render_task_tracker: заголовок/статус-строка соответствуют tasks.stage на каждой стадии (нет застывшего To Analyse) (AC-2.1)"
module: tests/test_notifications.py
expected: PASS
- id: TC-08
type: unit
description: "live-overlay рисует Deploying/Monitoring при наличии соответствующего Plane-UUID; деградирует на offline-label при ошибке/выкл. kill-switch (AC-3.2)"
module: tests/test_notifications.py
expected: PASS
# ---------------- BR-EFF: эффорт в карточке ----------------
- id: TC-09
type: unit
description: "миграция agent_runs.effort идемпотентна (_ensure_column дважды — без ошибки) (AC-E.1)"
module: tests/test_db.py
expected: PASS
- id: TC-10
type: unit
description: "launcher стампит resolve_agent_effort(agent) в agent_runs.effort в момент запуска; значение = фактический --effort (AC-E.1)"
module: tests/test_launcher.py
expected: PASS
- id: TC-11
type: unit
description: "строка стадии рендерит эффорт рядом с моделью в выбранном формате; developer=xhigh, tester/deployer=medium, прочие=high (AC-E.2, AC-E.3)"
module: tests/test_notifications.py
expected: PASS
- id: TC-12
type: unit
description: "пустой/неизвестный effort → суффикс эффорта опускается, рендер не падает (AC-E.4)"
module: tests/test_notifications.py
expected: PASS
# ---------------- BR-G5: честное время ----------------
- id: TC-13
type: unit
description: "brd_review-окно ~6ч (искусственный застой) → итоговое 'твоё время' НЕ показывает ~6ч (отсечка/активные окна) (AC-5.1)"
module: tests/test_notifications.py
expected: PASS
- id: TC-14
type: unit
description: "agent-время = Σ _duration_seconds(agent_runs) точно; 💰-итоги без регресса (AC-5.2)"
module: tests/test_notifications.py
expected: PASS
- id: TC-15
type: unit
description: "итоговая строка done: wall помечен как 'общее (с ожиданием)' ИЛИ wall сходится с Σ(стадии)+Σ(паузы); числа согласованы (AC-5.3)"
module: tests/test_notifications.py
expected: PASS
# ---------------- never-raise / сквозные ----------------
- id: TC-16
type: unit
description: "update_task_tracker / render_task_tracker никогда не поднимают исключение при ошибке Telegram/БД (моки бросают) (AC-X.2)"
module: tests/test_notifications.py
expected: PASS
- id: TC-17
type: unit
description: "ссылки ORCH-067 (plane_issue_link кликабельный номер) и disable_web_page_preview (ORCH-080) сохранены в payload (AC-X.4)"
module: tests/test_notifications.py
expected: PASS
# ---------------- интеграция / воспроизведение ----------------
- id: TC-18
type: integration
description: "staging-прогон задачи (8501): на каждой стадии зафиксировать (заголовок+тело в Telegram) vs (stage в БД); в чате остаётся одна актуальная карточка без сирот (G0 воспроизведение, AC-0.2, AC-1.1)"
module: docs/work-items/ORCH-087/06-adr # фиксируется в ADR как таблица воспроизведения
expected: PASS
- id: TC-19
type: integration
description: "merge-gate: ветка поверх origin/main с ORCH-86; reconciler.py не эродирован (маркеры ORCH-086 на месте), pytest tests/ -q зелёный (AC-6.1, AC-6.2, AC-X.2)"
module: tests/
expected: PASS

View File

@@ -0,0 +1,288 @@
# ADR-001: Зачистка осиротевших трекер-карточек (bump + полный учёт message_id), эффорт в строке стадии, честное итоговое время
## Статус
Accepted
## Контекст
Каждая задача имеет ОДНУ live-карточку в Telegram (`update_task_tracker`, инвариант
«одна карточка на задачу»). Дефолтный режим — `bump` (ORCH-067/042): на каждом
обновлении старая карточка удаляется и новая шлётся вниз чата (фича-просьба Славы —
«карточка всегда внизу»). Указатель `tasks.tracker_message_id`**скаляр**, хранит
ТОЛЬКО последний `message_id`.
**Симптом (скриншот Славы, 08.06, ORCH-082):** в чате висела карточка с заголовком
`📍 To Analyse`, хотя задача прошла весь конвейер до стадии `deploy`; статусы
deploy-цикла не отражены. Карточка — **осиротевшая** старая (`msg 18204`),
застрявшая на первом рендере (`To Analyse` = `_DEFAULT_STATUS_LABEL`). Проверено
(`deleteMessage → ok:true` и для 18204, и для 18227): бот ИМЕЕТ право удалять — дело
не в правах, а в **потере ссылки** на старые `message_id`.
BRD требует (BR-G0): сначала расследование → ADR, потом фикс. Ниже — ответы на все
4 вопроса §4 BRD, рекомендация и принятые архитектурные решения.
---
## G0 — Ответы на вопросы расследования (BR-G0, AC-0.1)
### Вопрос 1 — Сколько РЕАЛЬНО карточек одной задачи висело
По логам/скриншоту ORCH-082 подтверждено **минимум 2 живых сообщения** одной задачи
(`18204` — осиротевшая «замёрзшая» на `To Analyse`; `18227` — актуальная). Скалярный
указатель структурно допускает **N>1** сирот: каждый рассинхрон (см. вопрос 2) теряет
ровно одну ссылку, а сиротство накопительно — за прогон из ~8 переходов в худшем
случае осиротеть может до N1 карточек. Точное число для конкретного прогона
непредсказуемо именно потому, что учёта старых mid НЕТ — это и есть корень бага.
### Вопрос 2 — В какие МОМЕНТЫ `tracker_message_id` рассинхронизируется
Текущий код (`update_task_tracker`, ветка `mode == "bump"`):
```python
if mid is not None:
delete_telegram(mid) # best-effort, результат НЕ гейтит send (BR-6)
new_mid = send_telegram(text, disable_notification=True)
if new_mid is not None:
set_tracker_message_id(task_id, new_mid) # перепонт ТОЛЬКО на новый mid
```
| Сценарий | Механика | Рождает сироту? |
|----------|----------|-----------------|
| (a) `send``None` (нет креды / transient) | `new_mid is None` → указатель НЕ перезаписан; но `delete(old)` уже выполнен best-effort. Старая удалена (или осталась, если delete тоже упал — см. e). | Сам по себе — нет; защита BR-6 корректна. |
| (b) рестарт орка между `delete` и `send` | `delete(old)` прошёл, процесс упал до `send` → при перезапуске рисуется новая, старая уже удалена. | Обычно нет; но если `delete` вернул False до падения — old жив, ссылка на него только в скаляре, который не менялся → следующий bump его подчистит. |
| (c) пересоздание карточки во время CLI-фикса / ручных операций | Ручной `sendMessage` или внешняя правка вне `update_task_tracker` создаёт mid, которого нет в учёте. | Да — учёт о нём не знает. |
| (d) **гонка** двух `update_task_tracker` подряд (быстрые стадии) | Оба читают один `mid`, оба `delete` его (один `ok`, второй `already gone`→True), оба `send`**две** новых карточки; указатель садится на одну → вторая осиротела. | **Да** — частый на быстрых стадиях. |
| (e) **`delete` упал (transient/>48ч), но `send` прошёл** | `delete(old)` → False (old жив), `send` → new, указатель `=new` → ссылка на old **навсегда потеряна**. | **Да — доминирующий генератор сирот.** |
**Вывод:** доминируют (d) гонка и (e) delete-fail+send-ok. Общий первопричинный
дефект — **скалярный учёт**: система знает лишь о последнем `message_id`, поэтому при
любой потере ссылки старая карточка осиротевает безвозвратно.
### Вопрос 3 — Почему ИМЕННО заголовок застывает на `To Analyse`
Это **старый рендер**, а НЕ баг план-лейбла. Код-аудит подтверждает:
`render_task_tracker``_card_status_label``plane_status_label` детерминированно
выводит заголовок из `tasks.stage` (`_STAGE_STATUS_LABEL`), и на `deploy` корректно
даёт `⏸️ Awaiting Deploy`. Осиротевшая карточка `18204` была отрисована ОДИН раз на
самой ранней стадии (`stage` ещё `created`/`analysis``To Analyse` =
`_DEFAULT_STATUS_LABEL`) и больше не редактировалась/не удалялась (ссылка потеряна).
Рендер исправен; «замёрзший» заголовок — следствие сиротства (G1), а не G2.
**Таблица воспроизведения «стадия → (заголовок в Telegram) vs (stage в БД)»**
(аналитическая, выведена из кода `plane_status_label`/`_STAGE_STATUS_LABEL`; подлежит
подтверждению живым staging-прогоном TC-18 на 8501, AC-0.2):
| `tasks.stage` (БД) | Заголовок актуальной карточки (ожидаемо) | Заголовок ОСИРОТЕВШЕЙ (факт ORCH-082) |
|--------------------|------------------------------------------|----------------------------------------|
| created | `📍 To Analyse` | `📍 To Analyse` |
| analysis | `📍 Analysis` (или `⏸️ In Review` при открытом brd-clock) | `📍 To Analyse` (замёрзла) |
| architecture | `📍 Architecture` | `📍 To Analyse` |
| development | `📍 Development` | `📍 To Analyse` |
| review | `📍 Code-Review` | `📍 To Analyse` |
| testing | `📍 Testing` | `📍 To Analyse` |
| deploy | `📍 ⏸️ Awaiting Deploy — ожидание Confirm Deploy` (+overlay `Deploying`/`Confirm Deploy`/`Monitoring`) | `📍 To Analyse` |
| done | `🎉 … ГОТОВО` + `📍 Done` | `📍 To Analyse` |
Правый столбец — наглядное доказательство: одна карточка отстаёт на `stage` в БД
ровно потому, что потеряла ссылку и больше не обновляется.
### Вопрос 4 — `bump` vs `edit`: что надёжнее против сирот
| Критерий | `edit` (правка in-place) | `bump` (delete+send вниз) |
|----------|--------------------------|----------------------------|
| Сироты by design | **Нет** (одно сообщение редактируется) | **Да** при рассинхроне (вопрос 2) |
| «Карточка всегда внизу» (фича-просьба ORCH-042) | Теряется (карточка тонет вверх чата) | **Сохраняется** |
| Реакция на потерю ссылки | EDIT_GONE → один новый mid, старый и так недоступен | старый mid терялся → сирота |
| Поведение при гонке (d) | оба правят один mid (idempotent) | два новых сообщения |
`edit` строго надёжнее против сирот, но **регрессирует явную фича-просьбу** Славы
(«карточка внизу», ради которой bump и сделан дефолтом в ORCH-067). `bump` плодит
сирот **только** из-за скалярного учёта — устранимого первопричинного дефекта, а не
неотъемлемого свойства режима.
**Рекомендация (обоснованная данными): сохранить `bump` дефолтом и устранить
первопричину — вести ПОЛНЫЙ учёт незакрытых `message_id` (вариант A из R-5).** Это
даёт и фичу «карточка внизу», и отсутствие сирот. Переход на `edit` (вариант B) был
бы откатом UX-решения ORCH-067 ради лечения симптома, а не причины. `edit` остаётся
доступен через `ORCH_TRACKER_MODE=edit` (kill-switch неизменен).
---
## Решение
### Р-1 (G1) — bump + полный учёт message_id через таблицу-леджер `tracker_messages`
Вводится **аддитивная таблица-леджер** всех незакрытых карточек задачи (вариант A1
из R-5; выбран над JSON-массивом A2 — см. «Альтернативы»):
```sql
CREATE TABLE IF NOT EXISTS tracker_messages (
task_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
deleted_at TEXT, -- NULL = карточка ещё жива (незакрыта)
PRIMARY KEY (task_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_tracker_messages_open
ON tracker_messages(task_id) WHERE deleted_at IS NULL;
```
Скаляр `tasks.tracker_message_id` **сохраняется** (обратная совместимость: остаётся
указателем на ТЕКУЩУЮ карточку для прочих читателей `get_tracker_message_id`).
Леджер — авторитетный источник для зачистки.
**Алгоритм `update_task_tracker`, ветка `bump` (соблюдает R-1…R-6):**
1. Прочитать ВСЕ незакрытые mid задачи: `SELECT message_id FROM tracker_messages
WHERE task_id=? AND deleted_at IS NULL` (R-1).
2. Для каждого: `delete_telegram(mid)`:
- `True` (удалено ИЛИ `_DELETE_GONE_MARKERS` «already gone», вкл. >48ч) →
`UPDATE … SET deleted_at=datetime('now')` (исключить из учёта, R-2);
- `False` (transient/сеть/5xx) → оставить незакрытой для повторной попытки на
следующем bump (R-2).
3. `new_mid = send_telegram(text, disable_notification=True)` — РОВНО один send (R-4).
4. Если `new_mid is not None`: `INSERT INTO tracker_messages(task_id, message_id)`
**и** `set_tracker_message_id(task_id, new_mid)`. Если `None` — НЕ трогать ни
леджер, ни указатель (R-3, сохранена защита BR-6).
**Инвариант (R после фикса):** после любого `update_task_tracker` все ранее созданные
карточки задачи либо удалены, либо помечены `deleted_at`, либо остались незакрытыми
для повторной попытки — НИ ОДНА не теряется из учёта (в пределах 48ч-лимита Telegram).
**Совместимость / миграция:** на первой инициализации существующий
`tasks.tracker_message_id` НЕ переносится автоматически в леджер (одноразовый бэкфилл
не требуется — старые сироты всё равно за 48ч-окном). Новый поток ведёт леджер с
нуля; никаких изменений данных enduro-trails.
**Зачистка delete ДО send** (как в текущем коде): момент пустоты тих
(`disable_notification`), приемлем.
### Р-2 (G1, остаточный риск гонки) — самозалечивание, без блокировок
Гонка (d) двух одновременных `update_task_tracker` (вызываются из queue-worker,
reconciler, reaper) может на ОДИН цикл оставить лишнюю карточку: оба прочитали тот же
открытый набор, оба отправили новую. Обе новые попадают в леджер → **следующий** bump
их зачистит. Это строго лучше текущего ПОСТОЯННОГО сиротства и **самозалечивается** за
один переход. Кросс-процессную сериализацию (файловый лок/транзакция) НЕ вводим:
контракт компонента — best-effort, never-raise, карточка silent; цена лока не
оправдана. Остаточный риск задокументирован (AC-1.4, §Последствия).
### Р-3 (G2) — заголовок текущей стадии
Отдельного кода не требует: после Р-1 в чате остаётся ОДНА живая карточка, а
`render_task_tracker`/`plane_status_label` уже выводят заголовок из `tasks.stage`.
Закрепляется регресс-юнитом: `plane_status_label` перебирает все стадии
`created…done` и даёт корректный лейбл (TC-06, AC-2.2).
### Р-4 (G3) — deploy-цикл на карточке
- `_STAGE_STATUS_LABEL["deploy"] = "⏸️ Awaiting Deploy — ожидание Confirm Deploy"`
(offline) — присутствует, покрывает AC-3.1.
- live-overlay `_live_plane_branch_override` рисует `Deploying` / `Monitoring after
Deploy` через `_LIVE_BRANCH_LABELS` при наличии выделенного Plane-UUID — покрывает
AC-3.2.
- **Добавить (полнота цикла):** ключ `"confirm_deploy": "⏳ Confirm Deploy —
подтвердите прод-деплой"` в `_LIVE_BRANCH_LABELS` (логический ключ `confirm_deploy`
уже существует в `plane_sync` с ORCH-059). Без base-alias (это реальный отдельный
статус). Контракт never-raise и kill-switch `tracker_live_status` сохранены.
- `Done` рендерится из `stage == "done"` (AC-3.3) — без изменений.
### Р-5 (BR-EFF) — эффорт в строке стадии
- **Схема:** новая колонка `agent_runs.effort TEXT` через
`_ensure_column(conn, "agent_runs", "effort", "TEXT")` рядом с `model` (аддитивно,
идемпотентно).
- **Стамп в момент запуска** (`launcher._spawn`): сразу после строки
`effort = resolve_agent_effort(agent, project_id)` (line 475) выполнить
`UPDATE agent_runs SET effort=? WHERE id=run_id` со значением `effort or None`
(РЕАЛЬНО ушедшее в `--effort`; пустое → `NULL` → суффикс опускается). Выбран
follow-up `UPDATE` (а не расширение `INSERT` на line 449) — минимальный диф, без
переноса резолва модели/эффорта выше по коду; значение точно соответствует флагу
запуска. CLI не возвращает эффорт в result-JSON, поэтому стамп — единственный
надёжный источник (BR §6).
- **Рендер** (`render_task_tracker._stage_line`): добавить `effort` в SELECT
`agent_runs` и в строку стадии **единым форматом `· {model} · {effort}`**
(напр. `✅ Разработка 12м · …↓/…↑ · $… · opus-4-8 · xhigh`). Пустой/неизвестный
эффорт → суффикс эффорта опускается (как опускается модель при пустой
`short_model_name`) — рендер не падает (AC-E.4). Допустим fallback на
`resolve_agent_effort(run["agent"])` для исторических строк без колонки.
- **Ожидаемо** (ORCH-41/081): developer=`xhigh`; tester/deployer=`medium`;
analyst/architect/reviewer=`high` (AC-E.3).
### Р-6 (BR-G5) — честное и сходимое итоговое время
Текущая строка `done` («магическое» раздутое число) заменяется на **три
независимых, явно подписанных метрики** — ни одна не выдаётся за сумму других
(удовлетворяет T-4 формулировкой «не показывать wall как сумму»):
```
⏱️ Агенты {agent_seconds} · твоё {review_capped} · общее с ожиданием {wall}
```
- **T-1 `agent_seconds`** = `Σ _duration_seconds(started, finished)` по `agent_runs`
— **главная метрика**, остаётся точной (без регресса).
- **T-2 `review_capped`** — человеческое BRD-время, ограниченное разумным порогом
`tracker_brd_review_cap_s` (новый config-флаг, env `ORCH_TRACKER_BRD_REVIEW_CAP_S`,
**дефолт 7200с = 2ч**). При `review_seconds > cap` отображается capped-значение с
маркером «~» (напр. `~2ч`), сигнализируя об отсечке аномального застоя/рассинхрона
(кейс ORCH-087: brd_review болтался открытым из-за In Review→Backlog desync,
показывал 392м). Выбран порог (а не «активные окна») — под-оконных данных у нас нет
(только `brd_review_started_at`/`ended_at`); порог — допустимый T-2 вариант.
Закрывает AC-5.1 (6ч-окно → не ~6ч).
- **T-3 `wall`** = `_duration_seconds(created_at, updated_at)` — подписан **«общее с
ожиданием»**, НЕ выдаётся за рабочее время. Включает очередь/ожидание/застой.
- **T-4** соблюдён: метрики независимы и явно подписаны; wall НЕ представлен как
`агенты + твоё` (несведение по незалогированным queue-паузам перестаёт «врать»).
- **T-5** `💰`-строка и агрегаты `total_in/out/cost` — без изменений.
### Р-7 (BR-G6) — свежий main / без эрозии reconciler
Подтверждено на стадии архитектуры: `git merge-base --is-ancestor origin/main HEAD`
→ true (origin/main содержит merge-коммит ORCH-086, #86); в `src/reconciler.py`
ветки присутствуют 43 маркера ORCH-086 (`skipped_terminal_total`, `state_uuid`,
terminal-skip). **Файлы ORCH-087 (`notifications.py`, `db.py`, `agents/launcher.py`,
`usage.py`, тесты) НЕ пересекаются с `reconciler.py`** → правки 86 не эродируются.
`CHANGELOG.md` правится под `.gitattributes merge=union`. Явная проверка на
merge-gate — AC-6.1/AC-6.2 (TC-19).
---
## Инварианты (не нарушаются)
- never-raise по всему пути нотификаций; карточка всегда silent (`disable_notification`).
- «одна карточка на задачу»; ≤1 `send` за вызов `update_task_tracker` (R-4).
- Ссылки ORCH-067 (`plane_issue_link`), `disable_web_page_preview` ORCH-080 — сохранены.
- `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / стадии конвейера — **без изменений**.
- Миграции БД аддитивны и идемпотентны (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`),
restart-safe на общей прод-БД; данные enduro-trails не трогаются.
## Альтернативы (отклонены)
- **Вариант B (переход дефолта на `edit`)** — устраняет сирот by design, но
регрессирует фича-просьбу «карточка внизу» (ORCH-042/067). Лечит симптом, а не
причину. Отклонён; `edit` остаётся опцией через kill-switch.
- **Вариант A2 (JSON-массив `tasks.tracker_message_ids`)** — компактнее, но
read-modify-write блоба сам подвержен lost-update при гонке (d) (два процесса
перезапишут JSON друг друга — ровно тот класс багов, что чиним). Строка-на-mid в
таблице с раздельными INSERT/UPDATE этого избегает и даёт `deleted_at` для ретрая
transient-delete + наблюдаемость. Отклонён в пользу A1.
- **Файловый/транзакционный лок против гонки (d)** — избыточен для best-effort
silent-карточки; леджер самозалечивается за один переход. Отклонён.
## Последствия
**Плюсы:**
- Уходит класс багов «замёрзшая сирота» — в чате ровно одна достоверная карточка.
- Сохранена фича «карточка всегда внизу» (bump-дефолт).
- Эффорт виден рядом с моделью; источник стампа надёжен (момент запуска).
- Итоговое время честно и подписано; «магическое» раздутое число устранено.
- Все изменения аддитивны/идемпотентны, kill-switch'и сохранены, машина стадий не тронута.
**Минусы / ограничения:**
- **Telegram-лимит 48ч:** сообщения старше 48ч удалить нельзя (`_DELETE_GONE_MARKERS`
классифицирует это как «gone» → исключаются из учёта). Совсем старые сироты (до
деплоя фикса) могут остаться навсегда — **known limitation** (AC-1.4).
- **Остаточная гонка (d):** одна лишняя карточка может прожить один переход до
самозалечивания на следующем bump (см. Р-2).
- Новая таблица + колонка + один config-флаг — небольшой прирост схемы (оправдан).
- Порог `tracker_brd_review_cap_s` — эвристика: легитимный человеческий review длиннее
2ч будет отображён как `~2ч`. Порог конфигурируем; компромисс «честность vs точность»
в пользу неинтроду­цирования аномального застоя в «твоё время».

View File

@@ -0,0 +1,86 @@
# Требования к схеме БД — ORCH-087
Все изменения — **строго аддитивные и идемпотентные** (`CREATE TABLE IF NOT EXISTS`
/ `_ensure_column`), restart-safe на живой ОБЩЕЙ прод-БД (SQLite). Данные
enduro-trails не трогаются. Существующие колонки/таблицы не ломаются. Точка врезки —
`src/db.py::init_db` (рядом с прочими `_ensure_column`/`executescript`).
---
## 1. Колонка `agent_runs.effort` (BR-EFF, обязательно)
```python
_ensure_column(conn, "agent_runs", "effort", "TEXT")
```
- Тип `TEXT`, nullable. Хранит РЕАЛЬНО ушедшее в `--effort` значение
(`low|medium|high|xhigh|max`) или `NULL`, если флаг не подавался (резолв вернул "").
- Заполняется в `launcher._spawn` сразу после `resolve_agent_effort(agent,
project_id)` через `UPDATE agent_runs SET effort=? WHERE id=run_id`
(`effort or None`).
- Читается в `render_task_tracker` (добавить `effort` в SELECT `agent_runs`).
- Исторические строки (до миграции) → `effort IS NULL` → суффикс эффорта в карточке
опускается; допустим fallback на `resolve_agent_effort(run["agent"])`.
- Идемпотентность: `_ensure_column` — no-op при уже существующей колонке (AC-E.1,
TC-09).
## 2. Таблица-леджер `tracker_messages` (BR-G1, вариант A1 ADR-001)
```sql
CREATE TABLE IF NOT EXISTS tracker_messages (
task_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
deleted_at TEXT, -- NULL = карточка ещё жива (незакрыта)
PRIMARY KEY (task_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_tracker_messages_open
ON tracker_messages(task_id) WHERE deleted_at IS NULL;
```
- Авторитетный учёт ВСЕХ созданных карточек задачи; `deleted_at IS NULL` ⇔ карточка
считается живой и подлежит зачистке на следующем bump.
- Логический FK на `tasks.id` без `REFERENCES` (зеркалит `jobs.task_id`/`job_deps`) —
миграция не падает на pre-existing БД.
- Частичный индекс `WHERE deleted_at IS NULL` — дешёвая выборка незакрытых mid в
горячем пути рендера/зачистки.
- `PRIMARY KEY (task_id, message_id)` — идемпотентность INSERT (повторный mid не
дублируется); защита от двойного учёта при гонке.
**Новые геттеры/сеттеры в `src/db.py` (предложение, точная сигнатура — за разработчиком):**
| Функция | Назначение |
|---------|-----------|
| `add_tracker_message(task_id, message_id)` | INSERT нового mid (после успешного `send`). `INSERT OR IGNORE` для идемпотентности. |
| `get_open_tracker_messages(task_id) -> list[int]` | Все `message_id` с `deleted_at IS NULL`. |
| `mark_tracker_message_deleted(task_id, message_id)` | `UPDATE … SET deleted_at=datetime('now')` для успешно удалённых / «already gone». |
Контракт — как у существующих хелперов БД (never-raise по месту вызова в
notifications: ошибка БД не валит конвейер).
### Сосуществование со скаляром `tasks.tracker_message_id`
- `tasks.tracker_message_id` **СОХРАНЯЕТСЯ** без изменения семантики — указатель на
ТЕКУЩУЮ карточку (читатели `get_tracker_message_id`/`set_tracker_message_id` не
трогаются). Обратная совместимость полная.
- Леджер `tracker_messages` — НАДмножество: источник истины для зачистки сирот.
- Одноразовый бэкфилл скаляра в леджер **не требуется** (старые сироты всё равно за
48ч-окном Telegram). Новый поток ведёт леджер с нуля.
## 3. Что НЕ меняется
- `tasks` (кроме отсутствия изменений — скаляр сохранён), `jobs`, `events`,
`job_deps`, прочие колонки `agent_runs` (`model`, токены, cost, exit_code) — без
изменений.
- Никаких `DROP`/`ALTER … DROP`/переименований/перетипизаций (SQLite-небезопасно на
живой БД).
- `STAGE_TRANSITIONS` / `QG_CHECKS` — вне зоны БД, не затрагиваются.
## 4. Идемпотентность и restart-safety (проверка)
- Двойной вызов `init_db` → без ошибок (`IF NOT EXISTS` / `_ensure_column` no-op) —
TC-09.
- Леджер переживает рестарт орка: незакрытые mid читаются из БД → следующий bump
подчищает старые карточки (TC-05, AC-1.3).
- Миграция на БД с существующими данными enduro: только добавляет колонку/таблицу,
данные нетронуты (AC-X.5).

View File

@@ -0,0 +1,29 @@
# Технические риски — ORCH-087
Зона изменений: `src/notifications.py`, `src/db.py`, `src/agents/launcher.py`,
`src/usage.py`, тесты. Машина стадий и QG не затрагиваются. Контракт компонента —
never-raise, карточка silent.
| ID | Риск | Вероятность / Влияние | Митигация |
|----|------|------------------------|-----------|
| R-1 | **Self-hosting:** задача правит инструмент в проде, обслуживающем enduro-trails из общей БД/очереди. Регресс пути нотификаций мог бы испортить наблюдаемость всех проектов. | Низк. / Сред. | never-raise сохранён по всему пути; обязательный `deploy-staging` (8501) гейт перед прод-деплоем; нотификации не на критическом пути конвейера (ошибка не валит стадии). |
| R-2 | **Telegram 48ч-лимит:** сироты старше 48ч неудаляемы → могут остаться навсегда. | Сред. / Низк. | Документировано как known-limitation (ADR §Последствия, AC-1.4); `_DELETE_GONE_MARKERS` классифицирует как «gone» → исключает из учёта, не зацикливает ретраи. Касается только сирот ДО деплоя фикса. |
| R-3 | **Гонка (d)** двух `update_task_tracker` (queue-worker / reconciler / reaper) → лишняя карточка на один переход. | Сред. / Низк. | Леджер самозалечивается на следующем bump (ADR Р-2); строго лучше текущего постоянного сиротства; кросс-процессный лок сознательно не вводится (цена > выгоды для silent-карточки). |
| R-4 | **Миграция на живой общей прод-БД** (SQLite). Неаддитивная правка могла бы тронуть данные enduro. | Низк. / Выс. | Только `CREATE TABLE IF NOT EXISTS` / `_ensure_column` (идемпотентно, no-op при существовании); никаких DROP/ALTER DROP/переименований; логический FK без `REFERENCES` (не падает на pre-existing БД). TC-09 проверяет идемпотентность. |
| R-5 | **BR-G6 / merge-gate:** ветка должна жить поверх свежего `origin/main` (ORCH-86); эрозия `reconciler.py` затёрла бы terminal-skip/`state_uuid`-dedup. | Низк. / Выс. | Подтверждено: origin/main — предок HEAD; 43 маркера ORCH-086 на месте; файлы ORCH-087 НЕ пересекают `reconciler.py`. `CHANGELOG.md` под `.gitattributes merge=union`. Явная проверка merge-gate — TC-19 (AC-6.1/6.2). |
| R-6 | **Порог `tracker_brd_review_cap_s`** (дефолт 2ч): легитимный человеческий BRD-review длиннее 2ч отобразится как `~2ч` (недо-отчёт). | Сред. / Низк. | Конфигурируем (env); компромисс в пользу неинтродуцирования аномального застоя в «твоё время». Маркер `~` сигнализирует отсечку. Главная метрика (агенты) остаётся точной. |
| R-7 | **Стамп эффорта в `_spawn`:** доп. `UPDATE agent_runs` сразу после INSERT мог бы упасть и сорвать запуск агента. | Низк. / Сред. | `UPDATE` по существующему `run_id` в уже открытом соединении; в худшем случае effort=NULL → суффикс опускается (рендер не падает, AC-E.4). Эффорт — наблюдаемость, не функциональность запуска. |
| R-8 | **Регресс существующих тестов нотификаций** (новый формат строки стадии с эффортом + новая done-строка времени). | Сред. / Низк. | Обновить ожидания в `tests/test_notifications*.py`; новый формат строго аддитивен (суффикс эффорта/подписи времени). TC-11…TC-15. |
| R-9 | **Live-overlay `confirm_deploy`:** новый ключ overlay при отсутствии UUID статуса в проекте мог бы шуметь/падать. | Низк. / Низк. | overlay never-raise, деградирует на offline-label при отсутствии UUID/ошибке; kill-switch `tracker_live_status`; без base-alias (реальный отдельный статус). |
## Острые точки внимания для разработчика
1. **Порядок в bump:** зачистка ВСЕХ открытых mid из леджера → `send` → INSERT+repoint
ТОЛЬКО при `new_mid is not None` (R-3/BR-6). Ровно один `send` за вызов (R-4).
2. **never-raise:** любая ошибка БД-леджера / Telegram внутри `update_task_tracker`
гасится (как сейчас) — конвейер не падает (TC-16, AC-X.2).
3. **Эффорт = фактический флаг:** хранить `resolve_agent_effort(...)` как ушло в
`--effort` (пусто → NULL), а не пересчёт постфактум (CLI не возвращает эффорт).
4. **Не трогать** `reconciler.py`/`tests/test_reconciler.py` (BR-G6).
5. **Сохранить** `plane_issue_link` (ORCH-067) и `disable_web_page_preview` (ORCH-080)
в payload (TC-17, AC-X.4).

View File

@@ -0,0 +1,72 @@
---
type: review
work_item_id: ORCH-087
verdict: APPROVED
version: 1
---
# Review ORCH-087
## Summary
Задача закрывает три проблемы live-трекера: (G1) осиротевшие «замёрзшие» карточки,
(BR-EFF) эффорт в строке стадии, (BR-G5) честное итоговое время, плюс попутный CI-фикс
пути per-run логов. Реализация соответствует ТЗ, ADR-001 и критериям приёмки. Все 1090
тестов зелёные. Документация (CLAUDE.md, README.md, docs/architecture/README.md,
CHANGELOG.md, ADR) обновлена в том же PR. Машина стадий и реестр QG не тронуты; миграции
аддитивны/идемпотентны; never-raise сохранён. Найдена одна косметика P3 (неточный
inline-комментарий), не влияющая на поведение. Блокеров нет.
## Соответствие ТЗ / ADR
- **G1 (BR-G1, AC-1.x):** аддитивный леджер `tracker_messages(task_id, message_id,
created_at, deleted_at)` + хелперы `add_tracker_message` / `get_open_tracker_messages` /
`mark_tracker_message_deleted` (`src/db.py`). На каждом bump зачищаются ВСЕ незакрытые
mid (union скаляр+леджер). Контракт `delete_telegram` (True=gone вкл. `_DELETE_GONE_MARKERS`,
False=transient) совпадает с логикой `if delete_telegram(old): mark_deleted(...)`;
transient остаётся открытым для ретрая. Новый mid в леджер ТОЛЬКО при `send is not None`
(R-3/BR-6). Скаляр `tracker_message_id` сохранён (BC). ✔ соответствует ADR §G1 (вариант A1).
- **G3 (AC-3.x):** ключ `confirm_deploy` добавлен в `_LIVE_BRANCH_LABELS` — цикл
`Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done` полон. ✔
- **BR-EFF (AC-E.x):** колонка `agent_runs.effort TEXT` (`_ensure_column`, идемпотентно);
стамп фактического `resolve_agent_effort` в `launcher._spawn` через `UPDATE` по `run_id`
(never-block, обёрнут try/except); рендер `· {model} · {effort}`, пустой → опускается. ✔
- **BR-G5 (AC-5.x):** done-строка переписана на три подписанных метрики
`⏱️ Агенты · твоё{~cap} · общее с ожиданием`; кап `tracker_brd_review_cap_s` (дефолт 2ч,
маркер `~`); `_capped_review_str` never-raise; agent-сумма не регрессировала. ✔
- **BR-G6 (AC-6.x):** `src/reconciler.py` / `tests/test_reconciler.py` НЕ тронуты;
`git merge-base --is-ancestor origin/main HEAD` → true; origin/main содержит merge ORCH-086;
маркеры ORCH-086 (`skipped_terminal_total`/`state_uuid`/terminal) на месте. ✔
- **Инварианты (AC-X.5):** `STAGE_TRANSITIONS` / `QG_CHECKS` без изменений; миграции
`CREATE TABLE/INDEX IF NOT EXISTS` + `_ensure_column` — аддитивны/идемпотентны (enduro не
трогается); `disable_notification` / `plane_issue_link` / `disable_web_page_preview` —
сохранены. ✔
- **ADR (AC-0.x):** ADR-001 отвечает на 4 вопроса §4 BRD, содержит таблицу staging-
воспроизведения и known-limitation Telegram 48ч (AC-1.4). Код фикса соответствует ADR. ✔
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice to have
- [ ] `src/notifications.py` (~стр. 460, докстринг блока рендера эффорта): комментарий
утверждает «Historical rows with NULL effort fall back to the config-resolved effort for
the agent», но `_run_effort` фолбэка на `resolve_agent_effort` НЕ делает — при пустом/NULL
effort возвращает `""` и суффикс опускается. Поведение корректно и соответствует AC-E.4
(fallback по ТЗ §5 был «Допустим», не обязателен); неточен лишь комментарий — стоит убрать
вводящую в заблуждение фразу или реально добавить фолбэк. Не влияет на работу.
## Документация
Обновлена в ТОМ ЖЕ PR (AC-X.3 выполнен):
- `CLAUDE.md` — §Нотификации/Telegram live-tracker (зачистка сирот, эффорт, честное время).
- `docs/architecture/README.md` — компонент Notifications + отдельный раздел ORCH-087.
- `README.md` — таблица env (`ORCH_RUNS_DIR`).
- `CHANGELOG.md` — `## [Unreleased]` запись (ORCH-087 трекер + CI-фикс пути логов).
- ADR `06-adr/ADR-001-tracker-orphan-cleanup.md` — присутствует, покрывает G0/механизм/формулу.
Замечаний по документации нет.

View File

@@ -0,0 +1,88 @@
---
type: test-report
work_item_id: ORCH-087
result: PASS
---
# Test Report — ORCH-087
Багфикс live-трекера: зачистка осиротевших карточек (G1), эффорт в строке стадии
(BR-EFF), честное итоговое время (BR-G5), плюс deploy-цикл на карточке (G3).
Review-вердикт `12-review.md`**APPROVED**. Прогнан полный регресс + smoke API.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: `feature/ORCH-087-orch-87-to-analyse-bump`
- Репозиторий: orchestrator (worktree)
- Прод-инстанс (8500): health `ok` — деструктивных операций не выполнялось
- Дата: 2026-06-09
## Результаты — тест-план (04-test-plan.yaml)
| TC ID | Описание | Тест(ы) | Результат |
|-------|----------|---------|-----------|
| TC-01 | bump удаляет ВСЕ незакрытые message_id, не только последний | `test_notifications_orphans.py` | PASS |
| TC-02 | send→None → учёт mid не теряется (BR-6/R-3) | `test_notifications_orphans.py` | PASS |
| TC-03 | delete=False (transient) остаётся в учёте; «already gone» исключается | `test_notifications_orphans.py` | PASS |
| TC-04 | повторные вызовы → одна живая карточка, ≤1 send, без дублей | `test_notifications_orphans.py` | PASS |
| TC-05 | учёт mid переживает «рестарт» (читается из БД) | `test_notifications_orphans.py` | PASS |
| TC-06 | plane_status_label детерминирован для created..done; deploy→Awaiting Deploy | `test_tracker_status_line.py` (parametrized) | PASS |
| TC-07 | заголовок/статус соответствуют tasks.stage (нет застывшего To Analyse) | `test_tracker_status_line.py` | PASS |
| TC-08 | live-overlay рисует Deploying/Monitoring; деградирует на offline при kill-switch | `test_tracker_status_line.py` | PASS |
| TC-09 | миграция agent_runs.effort идемпотентна (_ensure_column) | `test_launcher.py` / db-fallback тесты | PASS |
| TC-10 | launcher стампит resolve_agent_effort в agent_runs.effort при запуске | `test_launcher.py` (effort, 2 теста) | PASS |
| TC-11 | строка стадии рендерит эффорт рядом с моделью; dev=xhigh, tester/deployer=medium, прочие=high | `test_tracker_effort_time.py` | PASS |
| TC-12 | пустой/неизвестный effort → суффикс опускается, рендер не падает | `test_tracker_effort_time.py` | PASS |
| TC-13 | brd_review ~6ч (застой) → «твоё время» НЕ показывает ~6ч (cap) | `test_tracker_effort_time.py` | PASS |
| TC-14 | agent-время = Σ agent_runs точно; 💰-итоги без регресса | `test_tracker_effort_time.py` | PASS |
| TC-15 | done-строка: wall помечен «общее (с ожиданием)»; числа согласованы | `test_tracker_effort_time.py` | PASS |
| TC-16 | update_task_tracker/render никогда не raise при ошибке Telegram/БД | `test_tracker_status_line.py` / `test_notifications_orphans.py` | PASS |
| TC-17 | ссылки ORCH-067 (plane_issue_link) и disable_web_page_preview ORCH-080 сохранены | `test_tracker_issue_link.py` | PASS |
| TC-18 | staging-воспроизведение (G0): одна актуальная карточка без сирот | ADR-001 (таблица воспроизведения) | PASS (по ADR) |
| TC-19 | merge-gate: ветка поверх origin/main с ORCH-86; reconciler не эродирован; pytest зелёный | `git merge-base` + регресс | PASS |
## Критерии приёмки (03-acceptance-criteria.md)
- **G0 (AC-0.x):** ADR-001 присутствует, отвечает на 4 вопроса §4 BRD, содержит таблицу
staging-воспроизведения и known-limitation 48ч → PASS.
- **G1 (AC-1.x):** леджер `tracker_messages`, мульти-mid зачистка, send→None защита,
unit-покрытие зелёное → PASS.
- **G2/G3 (AC-2.x/3.x):** plane_status_label детерминирован для всех стадий; ключ
`confirm_deploy` в `_LIVE_BRANCH_LABELS`; deploy→Awaiting Deploy offline → PASS.
- **BR-EFF (AC-E.x):** колонка `agent_runs.effort` идемпотентна, стамп в `_spawn`,
рендер `· model · effort`, значения по ORCH-41/081 → PASS.
- **BR-G5 (AC-5.x):** три подписанных метрики, cap `tracker_brd_review_cap_s`,
agent-сумма точна → PASS.
- **BR-G6 (AC-6.x):** `git merge-base --is-ancestor origin/main HEAD` → TRUE;
`src/reconciler.py` — 35 вхождений маркеров ORCH-086 (`skipped_terminal_total`/
`state_uuid`), логика не эродирована → PASS.
- **Сквозные (AC-X.x):** одна карточка/≤1 send; полный pytest зелёный (never-raise);
доки обновлены; ссылки/preview сохранены; STAGE_TRANSITIONS/QG_CHECKS не тронуты → PASS.
## Smoke test API (прод 8500, read-only)
- `GET /health``{"status":"ok","service":"orchestrator"}`
- `GET /status` → отвечает; active_tasks включает ORCH-087 (stage `testing`)
- `GET /queue` → отвечает; `counts.running=1`, reconcile/reaper/post_deploy/merge_verify
блоки в норме; `skipped_terminal_total` присутствует (ORCH-086 наблюдаемость жива)
## Вывод pytest
```
============================= test session starts ==============================
platform linux -- Python 3.12.13, pytest-8.3.3
collected 1090 items
...
======================= 1090 passed, 1 warning in 29.87s =======================
```
(1 warning — PydanticDeprecatedSince20 в `src/config.py`, не связана с задачей.)
ORCH-087-специфичные модули (повторный прогон):
- `test_notifications_orphans.py` — 7 passed
- `test_tracker_effort_time.py` — 12 passed
- `test_tracker_status_line.py` — 18 passed
- `test_tracker_bump.py` + `test_tracker_bump_default.py` — 21 passed
- `test_launcher.py -k effort` — 2 passed
## Итог
**PASS** — все 1090 тестов зелёные, smoke API OK, все критерии приёмки выполнены,
инварианты (never-raise, одна карточка, STAGE_TRANSITIONS/QG неизменны, BR-G6
свежий main без эрозии reconciler) соблюдены. Задача готова к стадии deploy-staging.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-087
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,42 @@
---
staging_status: SUCCESS
timestamp: 2026-06-09T07:04:58Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (port 8501).
Run canonically inside the `orchestrator-staging` container (`docker exec`, ORCH-048 / ADR-001),
mode `stub`. Exit code **0** → advance.
## Verdict
- **Result:** 8/10 checks PASS, exit code 0.
- **REAL failed:** none.
- **SANDBOX_INFRA failed (waived, ORCH-061):** C9a, C9b.
```
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 | A1 GET /health → 200 status=ok | PASS |
| A | A2 GET /queue → 200 with counts/max_concurrency/resilience | PASS |
| A | A3 ORCH_STAGING=true (not prod) | PASS |
| B | B4 Plane: sandbox project accessible | PASS |
| B | B5 Gitea: orchestrator-sandbox accessible, push=true | PASS |
| B | B6 Registry: sandbox present, prod ET/ORCH absent | PASS |
| C | C7 Create issue in Plane SANDBOX | PASS |
| C | C8 Trigger pipeline via /webhook/plane | PASS |
| C | C9a Branch appears in orchestrator-sandbox | FAIL (waived: SANDBOX_INFRA) |
| C | C9b Analyst job enqueued in staging queue | FAIL (waived: SANDBOX_INFRA) |
The two waived failures are the known sandbox-infra checks (C9a/C9b) that depend on SANDBOX
bot accounts being members of the sandbox project — not on the pipeline. All REAL checks are
green, so the suite exits 0 (fail-closed for REAL checks is preserved). Cleanup ran: Plane
test issue deleted (HTTP 204), no orphan branch.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-88 [ЭПИК]: пакетный автономный режим (10-20 задач за ночь) — последовательно → потом параллельно
Work Item ID: ORCH-088
## Description
TBD

View File

@@ -0,0 +1,145 @@
# 01 — BRD: ORCH-088 — Пакетный автономный режим (Этап 1: serial e2e)
Work Item: **ORCH-088**
Repo: **orchestrator** (self-hosting)
Стадия: analysis
Заказчик: Слава
Тип: ЭПИК — Этап 1 (минимальный, без параллелизма)
> ⚠️ **Скоп зафиксирован Владельцем 09.06.** Реализуется ТОЛЬКО serial e2e (FR-1…FR-5).
> Фазовый режим A/B/C, merge-очередь FIFO, pre-merge rebase и зависимость от ORCH-83 —
> **ОТМЕНЕНЫ, не реализовывать.**
---
## 1. Бизнес-контекст и проблема
### 1.1. Цель эпика
Дать оркестратору **масштаб автономности**: накидать вечером 1020 задач и получить к утру
последовательно проведённый через весь конвейер (analysis → … → deploy → done) пакет — без
ручного запуска каждой задачи и без взаимного повреждения веток.
### 1.2. Корневая проблема — «stale-анализ» (логический, а не код-затирание)
Конвейер создаёт ветку задачи от `main`. Если задача **N+1** входит в анализ, пока задача **N**
ещё **не влита в `main`**, то ветка N+1 срезается от **устаревшего** `main` (без кода N). Результат:
- семантически устаревшая база разработки;
- риск потери/переоткрытия уже сделанного в N (накопительные потери прецедента — постмортем
фантомного merge, см. CLAUDE.md / ORCH-071);
- ручной разбор конфликтов утром вместо готового пакета.
Физическое **код-затирание** при параллельном merge уже закрыто (ORCH-026 auto_rebase + merge-lease).
ORCH-088 закрывает **логический** разрыв: гарантирует, что каждая следующая задача стартует от
`main`, **уже содержащего все предыдущие завершённые задачи репо**.
### 1.3. Почему сериализация именно «от АНАЛИЗА», а не «от merge»
Ветка срезается в самом начале — на входе в анализ (`start_pipeline` создаёт ветку в Gitea, далее
worktree). Если допустить параллельный анализ N и N+1, ветка N+1 уже срезана от старого `main`
поздняя сериализация на merge проблему не лечит. Поэтому gate ставится на **входе новой задачи в
анализ**: новая задача не начинает анализ (и не режет ветку), пока в репо есть незавершённая задача.
### 1.4. Установленные факты (проверено, не изобретать)
- **Plane API v1:** bulk-операций НЕТ; issue-relation НЕТ → зависимости/очередь оркестратор хранит
**у себя** (gate в планировщике/claim по локальной БД), не в Plane.
- **Уже есть (переиспользовать):** `max_concurrency=1`; ORCH-026 auto_rebase_onto_main +
force-with-lease + merge-lease; персистентная очередь ORCH-1 (таблица `jobs`, atomic claim,
restart-safe); ORCH-021 post-deploy monitor (для self — всегда `ALERT_ONLY`, db-стадия `done`
достигается ДО окна мониторинга — ORCH-071/066).
### 1.5. Решения Владельца (09.06) — приняты как требования
| # | Решение |
|---|---------|
| D-1 | Serial e2e подтверждён. BRD появляются **по одному** — осознанный размен: надёжность > батч-просмотр BRD. |
| D-2 | Сигнал «задача завершена» = **успешный прод-деплой** (`stage = done` после прод-деплоя). НЕ merge, НЕ staging. |
| D-3 | Мониторинг (~15 мин) **НЕ ждём**: gate N+1 открывается по `stage = done`, не по завершению окна мониторинга. |
| D-4 | Auto-rollback прода во время мониторинга → **заморозить gate + алерт**; следующая НЕ стартует до ручного снятия. |
| D-5 | Зависимость ORCH-088 ← ORCH-83 **убрана** — запускается независимо. |
---
## 2. Объём (scope)
### 2.1. В объёме (Этап 1)
- **FR-1 — Serial gate (per-repo):** новая задача не входит в `analysis` (не режет ветку, не
запускает analyst), пока в том же репо есть незавершённая задача (`stage < done`).
- **FR-2 — Очередь e2e:** накиданные задачи становятся в очередь и обрабатываются **строго по одной**
end-to-end (от анализа до прод-деплоя).
- **FR-3 — Per-repo изоляция:** сериализация действует **внутри одного репо**; разные репо
(`orchestrator`, `enduro-trails`) идут **параллельно** (независимые `main`).
- **FR-4 — Restart-safe:** активная задача и состояние gate определяются по **БД** (не in-memory) —
переживают рестарт оркестратора.
- **FR-5 — Rollback-freeze:** auto-rollback / деградация прода → gate репо **заморожен** + Telegram-
алерт; следующая задача не стартует до **ручного** снятия заморозки.
### 2.2. Вне объёма (явно, не делать)
- Merge-очередь FIFO; pre-merge rebase как отдельная фича; фазовый режим A/B/C; любая координация
**параллелизма** задач внутри одного репо.
- Изменение `STAGE_TRANSITIONS`, реестра `QG_CHECKS`, новых стадий конвейера.
- Зависимость от ORCH-83.
---
## 3. Заинтересованные стороны
- **Владелец/оператор (Слава):** накидывает пакет вечером, разбирает заморозку при сбое, читает
алерты, снимает freeze вручную.
- **Self-hosting прод (`orchestrator`):** обслуживает enduro-trails из того же инстанса — нельзя
ронять/блокировать конвейер enduro (FR-3).
---
## 4. Бизнес-требования (BR)
| ID | Требование | Связь |
|----|------------|-------|
| BR-1 | Пока в репо есть задача со `stage < done`, любая **другая** задача того же репо не начинает анализ — ждёт в очереди. | FR-1, AC-1 |
| BR-2 | Как только активная задача достигла `stage = done` (после прод-деплоя), следующая задача того же репо **автоматически** стартует анализ. | FR-1/FR-2, AC-2, D-2 |
| BR-3 | Ветка новой задачи срезается от `main`, **уже содержащего все ранее завершённые задачи репо** — нет stale-base. Branch не создаётся раньше, чем предшественник завершён. | FR-1, AC-6, §1.2 |
| BR-4 | Сериализация — строго per-repo; задачи разных репо идут параллельно, gate одного репо не влияет на другой. | FR-3, AC-4 |
| BR-5 | Активная задача и факт заморозки определяются из БД; после рестарта оркестратора gate ведёт себя идентично (не «забывает» активную задачу и не «теряет» freeze). | FR-4, AC-3 |
| BR-6 | Auto-rollback/деградация прода (post-deploy) → per-repo freeze + Telegram-алерт; следующая задача не стартует до ручного снятия freeze. | FR-5, AC-5, D-4 |
| BR-7 | Мониторинг прода (~15 мин) gate **не ждёт** — открытие gate привязано к `stage = done`. (Freeze BR-6 — отдельный, независимый от `stage` сигнал, т.к. к моменту деградации задача уже `done`.) | D-3, AC-5 |
| BR-8 | Поведение управляется kill-switch'ом и областью репо (как ORCH-35/43/58): выключение флага → строго прежнее поведение (нулевая регрессия для enduro). | NFR |
| BR-9 | Состояние gate наблюдаемо в `GET /queue` (активная задача репо, очередь ожидающих, статус freeze). | NFR |
---
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| NFR-1 | **never-raise:** любая ошибка логики gate не роняет claim/конвейер. Поведение при ошибке БД — **fail-open** для claim (транзиентный сбой не должен заклинить очередь ВСЕХ проектов), **fail-closed** для freeze (сомнение в безопасности прода → не стартовать). |
| NFR-2 | **Offline-устойчивость:** проверка gate в горячем цикле claim не должна ходить в сеть (Plane/Gitea) — иначе встанет очередь всех проектов. Источник истины — локальная БД. |
| NFR-3 | **Restart-safe:** никакого in-memory состояния; freeze и активная задача — в БД. |
| NFR-4 | **Нулевая регрессия:** при выключенном флаге запрос claim и путь старта идентичны текущим; enduro не затрагивается. |
| NFR-5 | **Инварианты неизменны:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate, схема post-deploy — не меняются (допустима только аддитивная, идемпотентная миграция БД). |
| NFR-6 | **Self-hosting безопасность:** механизм не рестартит/не роняет прод-контейнер; freeze — пассивная остановка стартов, не действие над прод. |
---
## 6. Допущения и ограничения
- `max_concurrency = 1` остаётся (Этап 1 без параллелизма); gate не зависит от значения, но не
ослабляет его.
- «Завершена» = `tasks.stage = 'done'`. Для self-hosting `done` достигается merge-verify + прод-деплой
(ORCH-071/036); пост-деплойное окно мониторинга идёт **после** `done` и gate его не ждёт (BR-7).
- Задача в статусе **Blocked / Needs Input** имеет `stage < done` и, следовательно, **держит gate
закрытым** — это сознательное поведение (Этап 1): пока задача не доведена до прод или не закрыта
оператором, пакет не движется. (Поведение зафиксировать в AC; альтернатива — вне скопа.)
- Снятие freeze (BR-6) — **ручное** (оператор), автоматического разбора деградации нет.
---
## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md)
- AC-1 активная задача (`stage<done`) → новая не стартует анализ.
- AC-2 активная достигла `done` → следующая стартует автоматически.
- AC-3 gate переживает рестарт (состояние в БД).
- AC-4 разные репо идут параллельно.
- AC-5 auto-rollback → freeze + алерт, следующая не стартует до ручного снятия.
- AC-6 каждая ветка срезана от `main` со всеми предыдущими завершёнными задачами репо (нет stale-base).
---
## 8. Риски (детали — 10-tech-risks.md, заполняет архитектор)
- R-1: stale-base сохраняется, если ветка режется на входе (`_create_gitea_branch` в `start_pipeline`)
до завершения предшественника — gate обязан отсрочить **создание ветки**, а не только claim.
- R-2: gate, ошибочно fail-closed на транзиентной ошибке БД, заклинит очередь всех проектов.
- R-3: «вечный freeze» / залипшая активная задача в Blocked останавливает пакет — нужна наблюдаемость
и ручное снятие.

Some files were not shown because too many files have changed in this diff Show More