Compare commits

...

103 Commits

Author SHA1 Message Date
post-deploy-monitor
855efeb63d docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-074
All checks were successful
CI / test (push) Successful in 25s
2026-06-08 22:29:26 +03:00
deploy-finalizer
b371b6d940 deploy(ORCH-036): finalize SUCCESS for ORCH-074
All checks were successful
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 24s
2026-06-08 22:07:38 +03:00
ea094f5922 tester(ET): auto-commit from tester run_id=397
All checks were successful
CI / test (push) Successful in 29s
2026-06-08 22:00:54 +03:00
17258fb69e reviewer(ET): auto-commit from reviewer run_id=396 2026-06-08 22:00:54 +03:00
0873803faa feat(launcher): drop dead frontmatter model + validate model name (never-break)
G1: remove the dead `model:` line from all 6 .openclaw/agents/*.md prompts —
launcher never read it; config (agent_model_*) is the single source of truth.

G2: add is_valid_model helper (format check ^claude-…$) applied inside
resolve_agent_model's resolution cascade and at the inline --fallback-model
read in _spawn. An invalid name is logged and skipped to the next valid level
(in the limit: no --model flag), never passed to the CLI, never raises. Format
check chosen over an allowlist for forward-compatibility (ADR-001).

G3 (routing) and G4 (fallback) intentionally NOT enabled — all agents stay on
claude-opus-4-8; agent_fallback_model stays "".

Docs (golden source) updated in the same change: README model/effort table +
validation, CLAUDE.md, .env.example (ORCH_AGENT_MODEL_*/EFFORT_*/FALLBACK_MODEL),
CHANGELOG. Tests: test_agent_frontmatter_no_model.py (G1), extended
test_resolve_agent_model.py (G2 never-break).

Refs: ORCH-074
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 22:00:54 +03:00
0c240198e4 architect(ET): auto-commit from architect run_id=394 2026-06-08 22:00:54 +03:00
1e1811a4bc analyst(ET): auto-commit from analyst run_id=393 2026-06-08 22:00:54 +03:00
e89f7c7a11 analyst(ET): auto-commit from analyst run_id=392 2026-06-08 22:00:54 +03:00
0f82ebc1a7 docs: init ORCH-074 business request 2026-06-08 22:00:54 +03:00
d04be97c0e docs(ORCH-074): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 22:00:34 +03:00
b0e517c76a Merge pull request 'ORCH-026: task dependencies (B waits for A) + single-repo merge serialization' (#78) from feature/ORCH-026-b-a into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-08 19:47:58 +03:00
deploy-finalizer
662d2d6434 deploy(ORCH-036): finalize SUCCESS for ORCH-026
All checks were successful
CI / test (push) Successful in 26s
2026-06-08 19:47:57 +03:00
deploy-finalizer
90a5cae8e6 deploy(ORCH-036): finalize FAILED for ORCH-026
All checks were successful
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-06-08 19:33:57 +03:00
deploy-finalizer
1d928dab57 deploy(ORCH-036): finalize FAILED for ORCH-026
All checks were successful
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-06-08 19:28:41 +03:00
9800dc89e3 tester(ET): auto-commit from tester run_id=390
All checks were successful
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 27s
2026-06-08 19:17:44 +03:00
5b80f8facb reviewer(ET): auto-commit from reviewer run_id=389 2026-06-08 19:17:44 +03:00
a74379f657 feat(ORCH-026): task dependencies (B waits for A) + single-repo merge serialization
Level A — merge/deploy serialization within one repo: reuse the existing
ORCH-043/065 merge-lease (no new mechanism); the only new logic is an
unconditional pre-merge rebase in check_branch_mergeable — under the held
lease, auto_rebase_onto_main is ALWAYS called when premerge_rebase_always
(default True), not just when the branch is behind. No-op on an up-to-date
branch (rebase keeps HEAD, force-with-lease -> "Everything up-to-date", CI
not triggered). Kill-switch off -> ORCH-043 behaviour 1:1.

Level B — declarative task dependencies: additive job_deps table
(CREATE ... IF NOT EXISTS, no live-DB migration); claim_next_job gate
(NOT EXISTS) defers a job whose depends-on tasks are not yet 'done' without
occupying a max_concurrency slot; inert on empty job_deps -> zero regression.
New leaf src/task_deps.py (never-raise): is_task_ready (fail-open), DFS cycle
detection + Blocked/alert, declare/ingest_plane_relations (db source never
hits the network on the hot path), snapshot. Telegram waiting-line, /queue
observability, reconciler skip + cycle backstop, reaper untouched.

Invariants unchanged: STAGE_TRANSITIONS, QG_CHECKS registry (dep gate is a
claim_next_job врезка, not a registered QG), DB schema of existing tables,
HTTP endpoints; non-self repos remain a no-op on empty deps/scope.

Flags: ORCH_PREMERGE_REBASE_ALWAYS, ORCH_TASK_DEPS_ENABLED, ORCH_TASK_DEPS_SOURCE.
Docs: docs/architecture/README.md, CLAUDE.md, .env.example, CHANGELOG.md,
adr-0015. Tests: tests/test_orch026_*.py (64 tests); full suite 991 green.

Refs: ORCH-026

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 19:17:44 +03:00
9019e12d98 architect(ET): auto-commit from architect run_id=387 2026-06-08 19:17:44 +03:00
518d7d18c8 analyst(ET): auto-commit from analyst run_id=386 2026-06-08 19:17:44 +03:00
520bcafa73 docs: init ORCH-026 business request 2026-06-08 19:17:44 +03:00
9f7b6edb6d docs(ORCH-026): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 19:17:18 +03:00
1c3ecb973e Merge pull request 'ORCH-073: SHA-in-main merge-verify + main regression guard' (#77) from feature/ORCH-073-crit-main-orch-067-069 into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-08 16:54:01 +03:00
deploy-finalizer
1b45fa0008 deploy(ORCH-036): finalize SUCCESS for ORCH-073
All checks were successful
CI / test (push) Successful in 21s
2026-06-08 16:54:00 +03:00
1f0929838a tester(ET): auto-commit from tester run_id=384
All checks were successful
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 21s
2026-06-08 16:30:46 +03:00
7deb151ce5 reviewer(ET): auto-commit from reviewer run_id=383 2026-06-08 16:30:46 +03:00
aff334e82b fix(merge-gate): SHA-in-main as sole merge-verify criterion + main regression guard
Root-cause fix for main erosion (phantom merge): code of ORCH-067/069 reached
`done` while absent from origin/main (only their auto docs-PRs landed).

- FR-1: verify_merged_to_main confirms merge ONLY by `git merge-base
  --is-ancestor <validated_sha> origin/main`; the OR-branch pr_already_merged is
  removed (a merged PR no longer confirms). Empty SHA / git error -> False.
- FR-2: pr_already_merged demoted to merge_pr idempotency-guard; counts a PR only
  when merged & head.ref==<branch> & base.ref=="main" (explicit in-loop filter).
- FR-3: merge_pr selects the open code-PR by head==<branch> AND base==main.
- FR-5: new deterministic check_main_regression in _handle_merge_verify (after
  confirmed SHA-in-main, before done) verifies MAIN_REGRESSION_MARKERS still in
  origin/main; deterministic count==0 -> alert "main regressed" + HOLD (NOT done,
  no rollback); git error of the grep -> fail-open. Kill-switch
  ORCH_REGRESSION_GUARD_ENABLED; non-self -> no-op.
- FR-4: root .gitattributes `CHANGELOG.md merge=union` so Unreleased edits
  auto-merge on rebase without conflict (branch not rolled back).

Invariants unchanged (STAGE_TRANSITIONS, QG_CHECKS, deploy-status, merge-gate,
image-freshness, DB schema, external HTTP API); non-self repos no-op (INV-5);
never-raise (INV-1); merge only via Gitea PR-API (INV-2).

Docs: CHANGELOG, .env.example (README/ADR updated by architect). Tests:
tests/test_orch073_*.py (TC-01..18); existing merge-gate tests updated for the
new code-PR filter.

Refs: ORCH-073

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 16:30:46 +03:00
fa9b96545c architect(ET): auto-commit from architect run_id=381 2026-06-08 16:30:46 +03:00
319b23b4fc analyst(ET): auto-commit from analyst run_id=380 2026-06-08 16:30:46 +03:00
e54d1fc4ac docs: init ORCH-073 business request 2026-06-08 16:30:46 +03:00
77abfb399c docs(ORCH-073): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Canonical run inside orchestrator-staging (ORCH-048): exit 0, all REAL
checks green; C9a/C9b waived as known sandbox-infra (ORCH-061).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 16:30:19 +03:00
05bd169b14 Merge pull request 'restore(main): re-merge ORCH-067 + ORCH-069 (ORCH-073)' (#76) from restore/orch-6769-2026-06-08 into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-08 15:04:47 +03:00
stream
183e6d68bc restore: re-merge ORCH-069 qg0_title_max
All checks were successful
CI / test (pull_request) Successful in 21s
# Conflicts:
#	CHANGELOG.md
2026-06-08 14:58:30 +03:00
stream
befa2979ec restore: re-merge ORCH-067 tracker bump+статусы+ссылки 2026-06-08 14:58:03 +03:00
deploy-finalizer
d33e0ded2e deploy(ORCH-036): finalize SUCCESS for ORCH-069
All checks were successful
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 20s
2026-06-08 11:44:38 +00:00
de70ee811d tester(ET): auto-commit from tester run_id=378
All checks were successful
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-06-08 11:30:17 +00:00
post-deploy-monitor
41da03470a docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-067
All checks were successful
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-06-08 11:28:18 +00:00
e1055861b5 reviewer(ET): auto-commit from reviewer run_id=377
All checks were successful
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 22s
2026-06-08 11:28:16 +00:00
2e84813c13 developer(ET): auto-commit from developer run_id=376
All checks were successful
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-06-08 11:25:09 +00:00
18f887c886 tester(ET): auto-commit from tester run_id=374
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 21s
2026-06-08 11:24:01 +00:00
37ef58f21f reviewer(ET): auto-commit from reviewer run_id=373 2026-06-08 11:24:01 +00:00
0b9ae514c9 docs(qg0): add ORCH_QG0_TITLE_MAX to README config table
Reviewer P1 fix (attempt 2/3): новый параметр отсутствовал в таблице
«Все переменные с префиксом ORCH_», делая её неконсистентной заголовку.
Закрывает AC-6 / ТЗ §9.

Refs: ORCH-069
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 11:24:01 +00:00
c56672aabf reviewer(ET): auto-commit from reviewer run_id=371 2026-06-08 11:24:01 +00:00
0ed05417e6 feat(qg0): configurable QG-0 title limit via ORCH_QG0_TITLE_MAX (default 200)
Replace the hardcoded `len(name) > 80` cap in the QG-0 entry validation
(_qg0_errors) with a configurable Settings.qg0_title_max (env
ORCH_QG0_TITLE_MAX, default 200). The 80-char cap was a hygiene limit, not
structural, so valid 81-200 char titles were rejected without a business
reason. The limit is read dynamically per call and the error text interpolates
the active value.

Graceful degradation (AC-3, self-hosting safety): an empty/non-numeric env
value no longer crashes the process on startup. A field_validator(mode="before")
intercepts the raw env before int-parsing and falls back to 200 (never raises),
suppressing pydantic ValidationError.

Additive and backward-compatible (default 200 > old 80). Invariants unchanged:
STAGE_TRANSITIONS, QG_CHECKS registry, DB schema, slug [:30], lower limits,
soft-QG-0 warning path, API.

Refs: ORCH-069

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 11:24:01 +00:00
7d99782673 architect(ET): auto-commit from architect run_id=369 2026-06-08 11:23:26 +00:00
59603f6e92 analyst(ET): auto-commit from analyst run_id=350 2026-06-08 11:23:26 +00:00
d5f11e5caa docs: init ORCH-069 business request 2026-06-08 11:23:26 +00:00
affbb259a1 Merge pull request 'docs(ORCH-069): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)' (#75) from docs/ORCH-069-staging-log into main 2026-06-08 14:22:30 +03:00
8149eb7769 docs(ORCH-069): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
All checks were successful
CI / test (pull_request) Successful in 21s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 11:22:22 +00:00
deploy-finalizer
9979eec168 deploy(ORCH-036): finalize SUCCESS for ORCH-067
All checks were successful
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 22s
2026-06-08 10:52:45 +00:00
c991b9de1a tester(ET): auto-commit from tester run_id=367
All checks were successful
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 24s
2026-06-08 10:34:33 +00:00
3d7d751b7a reviewer(ET): auto-commit from reviewer run_id=366 2026-06-08 10:34:33 +00:00
f330a580c4 docs(tracker): update CHANGELOG, CLAUDE.md, .env.example for ORCH-067
Закрывает P0/P1 ревью (attempt 2/3): документация = golden source.
- CHANGELOG.md: запись ORCH-067 в [Unreleased] (bump-дефолт, статус-строка
  карточки по модели ORCH-066, кликабельный номер задачи, новые флаги).
- CLAUDE.md: раздел «Нотификации / Telegram live-tracker» (ТЗ §5).
- .env.example: ORCH_TRACKER_MODE=bump (синхрон с новым дефолтом) +
  ORCH_TRACKER_LIVE_STATUS / _TTL_S / _TIMEOUT_S.

Refs: ORCH-067

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 10:34:33 +00:00
896ecf6acb reviewer(ET): auto-commit from reviewer run_id=364 2026-06-08 10:34:33 +00:00
096c452230 developer(ET): auto-commit from developer run_id=363 2026-06-08 10:34:33 +00:00
9f176036f1 architect(ET): auto-commit from architect run_id=362 2026-06-08 10:34:33 +00:00
3e4191050f analyst(ET): auto-commit from analyst run_id=361 2026-06-08 10:34:33 +00:00
38e329f6f7 docs: init ORCH-067 business request 2026-06-08 10:34:33 +00:00
58d6c433d1 docs(ORCH-067): staging gate verdict SUCCESS
Merge 15-staging-log.md artifact into main (staging gate passed, exit 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 10:34:16 +00:00
52ca882e5b Merge pull request 'feat: ORCH-071-crit-bug-merge-main' (#72) from feature/ORCH-071-crit-bug-merge-main into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-08 12:02:47 +03:00
d49e88cf3f tester(ET): auto-commit from tester run_id=359
All checks were successful
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 24s
2026-06-08 08:45:31 +00:00
e7a5b50f97 reviewer(ET): auto-commit from reviewer run_id=358 2026-06-08 08:45:31 +00:00
034343ec5d docs(changelog): add ORCH-071 merge-verify gate entry
Add CHANGELOG entry for the phantom-merge fix (merge-verify sub-gate,
deterministic merge actor, post-deploy verification, kill-switch).
Addresses P0 blocker from reviewer (attempt 2/3): docs = golden source
per CLAUDE.md §2/§6 and AC-5.

Refs: ORCH-071

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 08:45:31 +00:00
cc87beb2b4 reviewer(ET): auto-commit from reviewer run_id=356 2026-06-08 08:45:31 +00:00
fb25e9a0cf developer(ET): auto-commit from developer run_id=355 2026-06-08 08:45:31 +00:00
2824fd8543 architect(ET): auto-commit from architect run_id=354 2026-06-08 08:45:31 +00:00
c26a6b637c analyst(ET): auto-commit from analyst run_id=353 2026-06-08 08:45:31 +00:00
dd5fe619d5 docs: init ORCH-071 business request 2026-06-08 08:45:31 +00:00
f6b5671267 docs(ORCH-071): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Staging check suite passed against orchestrator-staging (8501), exit 0.
All REAL pipeline checks green; sandbox-infra C9a/C9b waived per ORCH-061.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 08:45:01 +00:00
49461238f1 Merge pull request 'restore(main): долить фантомные ORCH-022/059/066/068 (4 потерянных PR)' (#71) from integ/restore-main-2026-06-08 into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-08 09:57:18 +03:00
stream
c90c01b919 fix(tests): integ fixtures — to_analyse always present (066), full status keys, security_gate registered (022)
All checks were successful
CI / test (pull_request) Successful in 20s
2026-06-08 06:41:52 +00:00
stream
2ec6873e33 integ: merge ORCH-068 reconciler livelock fix
# Conflicts:
#	docs/architecture/README.md
#	src/reconciler.py
2026-06-08 06:36:29 +00:00
stream
cac6539698 integ: merge ORCH-066 plane status model
# Conflicts:
#	CHANGELOG.md
#	docs/architecture/README.md
#	src/plane_sync.py
#	src/webhooks/plane.py
2026-06-08 06:34:37 +00:00
stream
af7472df05 integ: merge ORCH-059 confirm-deploy
# Conflicts:
#	CHANGELOG.md
#	docs/architecture/README.md
2026-06-08 06:32:53 +00:00
stream
995ba0af71 integ: merge ORCH-022 security/secret-scanning 2026-06-08 06:32:13 +00:00
772ccab013 docs(history): CRITICAL postmortem — phantom merge (deploy without main-merge), see ORCH-071 2026-06-08 09:20:22 +03:00
post-deploy-monitor
06271b0bfb docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-068
All checks were successful
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 18s
2026-06-08 05:49:27 +00:00
101bd1c512 docs(history): lesson — Confirm Deploy dead trigger (ORCH-066 regression, see ORCH-070) 2026-06-08 08:38:30 +03:00
deploy-finalizer
aa4161fc78 deploy(ORCH-036): finalize SUCCESS for ORCH-068
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 20s
2026-06-08 05:34:23 +00:00
6bbd530caa tester(ET): auto-commit from tester run_id=351
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 21s
2026-06-08 05:18:46 +00:00
4b03f213f7 reviewer(ET): auto-commit from reviewer run_id=349 2026-06-08 05:18:46 +00:00
1d72c44587 fix(reconciler): stop F-2 livelock spam on synced terminal tasks + cache TTL
Reconciler F-2 spammed Telegram "<wi> разблокирована" every ~120s for a
fully-synchronized Done task (incident ET-002, 191+ msgs/night) after the
ORCH-066 Plane status model merge. Two stacked defects (defense in depth):

- D1 (selection): actionable states were told apart by bare UUID, so a Done
  issue aliased onto the approved UUID entered the approved branch. Now
  terminal states are excluded by Plane state GROUP (completed/cancelled),
  a project-independent discriminator robust to UUID aliasing; per-issue
  check with a logical-key fallback when the group is unavailable.
  get_project_states caches {uuid -> group} from the same /states/ fetch;
  new sibling accessor get_project_state_groups.
- D2 (notification): _note_unblock fired unconditionally after _dispatch.
  Now it only fires on a confirmed state change (stage before/after _dispatch;
  task-appears for the start case) — handlers' contracts untouched.
- TR-3: in-memory dedup guard {issue_id -> last unblocked state} as a backstop.
- TR-4: _STATES_CACHE lived for the whole process lifetime, so a new Plane
  status was invisible without a restart. Added TTL ORCH_PLANE_STATES_TTL_S
  (default 300s; 0 = previous lifetime cache) reusing reload_project_states();
  a failed refresh serves the stale-but-correct set, not enduro defaults.

STAGE_TRANSITIONS / QG_CHECKS / DB schema / handle_* contracts / F-1 / F-3
unchanged; never-raise preserved; self-hosting tick never restarts prod.
Observability: skipped_terminal_total / deduped_total in /queue reconcile block.

Tests: tests/test_reconciler_plane.py (TC-01..TC-10),
tests/test_plane_states_cache.py (TC-11/TC-12).

Refs: ORCH-068

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 05:18:46 +00:00
0605309602 architect(ET): auto-commit from architect run_id=347 2026-06-08 05:18:46 +00:00
044894cbe9 analyst(ET): auto-commit from analyst run_id=346 2026-06-08 05:18:46 +00:00
cb11137a77 docs: init ORCH-068 business request 2026-06-08 05:18:46 +00:00
48b54051e5 docs(ORCH-068): add staging gate log (staging_status: SUCCESS) 2026-06-08 05:18:24 +00:00
post-deploy-monitor
72d662ae88 docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-066
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-06-07 22:33:36 +00:00
deploy-finalizer
348cf8c164 deploy(ORCH-036): finalize SUCCESS for ORCH-066
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-06-07 22:18:32 +00:00
bc2347abd3 tester(ET): auto-commit from tester run_id=343
All checks were successful
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 18s
2026-06-07 22:02:45 +00:00
62c1fe3461 reviewer(ET): auto-commit from reviewer run_id=342 2026-06-07 22:02:45 +00:00
0dfddf93f0 feat(plane): осмысленная статусная модель Plane (слой B — индикация)
Приводит статусы доски Plane к смыслу стадий конвейера, сохраняя
инвариант «статус — индикация, а не управление». Меняется только слой B
(отображение: src/plane_sync.py + точки выставления статуса в
stage_engine.py/webhooks/plane.py/reconciler.py); слой A — машина стадий
src/stages.py::STAGE_TRANSITIONS — остаётся байт-в-байт неизменным (AC-21).

- 6 новых логических ключей статуса (to_analyse, analysis, code_review,
  awaiting_deploy, deploying, monitoring) + сеттеры и диспетчер
  set_issue_stage_state.
- Project-relative alias-fallback (BR-12): новый ключ деградирует на
  базовый UUID того же проекта → нулевая регрессия для enduro-trails.
- Самодеплой (ORCH-036) индицирует фазы: Awaiting Deploy / Deploying;
  terminal-sync для self-hosting → Monitoring after Deploy, для прочих →
  терминальный Done.
- Post-deploy монитор (ORCH-021): HEALTHY → Done, DEGRADED → Blocked
  (только индикация; self-hosting ALERT_ONLY, прод не трогается, BR-5).
- Reconciler: триггер старта/резюма на To Analyse; Guard 2 учитывает
  новые активные ожидания без расширения skip-set на алиасах.
- never-raise контракт сеттеров и резолвера состояний сохранён.
- Раскатка — созданием статусов в Plane оператором, без kill-switch.

Инварианты не менялись: STAGE_TRANSITIONS, QG_CHECKS (12 чеков),
check_deploy_status, exit-код-контракт хука, merge-gate, схема БД.

ADR: docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Тесты: test_plane_status_model, test_plane_to_analyse_resume,
test_plane_status_failclosed + TC в существующих наборах. 774 passed.

Refs: ORCH-066

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:02:45 +00:00
22d3b77426 architect(ET): auto-commit from architect run_id=340 2026-06-07 22:02:45 +00:00
4a06537afd analyst(ET): auto-commit from analyst run_id=339 2026-06-07 22:02:45 +00:00
b6c0e11e4d docs: init ORCH-066 business request 2026-06-07 22:02:45 +00:00
3fb3d15cb4 docs(ORCH-066): add staging gate log (staging_status: SUCCESS)
Some checks failed
CI / test (push) Has been cancelled
Staging check suite passed (8/10, exit 0): all REAL checks green;
C9a/C9b waived as known sandbox-infra (ORCH-061).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:02:33 +00:00
post-deploy-monitor
9f4d79baee docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-059
All checks were successful
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 19s
2026-06-07 19:44:39 +00:00
deploy-finalizer
7cdef6d377 deploy(ORCH-036): finalize SUCCESS for ORCH-059
All checks were successful
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-06-07 19:29:34 +00:00
ca41d9210b tester(ET): auto-commit from tester run_id=337
All checks were successful
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 17s
2026-06-07 19:20:41 +00:00
48943fe10a reviewer(ET): auto-commit from reviewer run_id=336 2026-06-07 19:20:41 +00:00
86fe8dd509 feat(deploy): dedicated "Confirm Deploy" status triggers prod deploy
Split the overloaded `Approved` Plane status: it served BOTH as the human BRD
gate on `analysis` AND as the silent Phase B prod-deploy trigger on `deploy`
(ORCH-036), so a routine approve could launch a self-hosting prod restart.

ORCH-059 introduces a dedicated logical status `confirm_deploy` ("Confirm
Deploy") that triggers ONLY Phase B on `deploy`; `Approved` stays purely a
pipeline gate.

- plane_sync: map "Confirm Deploy" -> "confirm_deploy" in _PLANE_NAME_TO_KEY;
  intentionally absent from _DEFAULT_STATES => fail-closed (no UUID -> .get
  yields None, no KeyError, no blind deploy).
- webhooks/plane: handle_issue_updated routes "Confirm Deploy" (fail-closed
  .get) to new handle_confirm_deploy (guarded to stage=="deploy") ->
  _try_advance_stage(confirm_deploy=True).
- stage_engine: advance_stage gains keyword-only confirm_deploy=False; Phase B
  block returns early for deploy+finished_agent is None but only initiates the
  deploy when confirm_deploy=True; a plain Approved is a deterministic no-op
  (returns before check_deploy_status -> no false БАГ-8 rollback).
- Phase A CTA now asks the operator for "Confirm Deploy", not "Approved".

Contracts unchanged: STAGE_TRANSITIONS, QG_CHECKS, check_deploy_status, hook
exit codes, Phases A/C, merge-gate, DB schema. Conditional like ORCH-35/36
(self-hosting only). Docs updated (CLAUDE.md, architecture/README.md, CHANGELOG).

Refs: ORCH-059

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 19:20:41 +00:00
dd07b58165 architect(ET): auto-commit from architect run_id=334 2026-06-07 19:20:41 +00:00
b67a61ecef analyst(ET): auto-commit from analyst run_id=333 2026-06-07 19:20:41 +00:00
8fcb867dcf docs: init ORCH-059 business request 2026-06-07 19:20:41 +00:00
4815e378d9 docs(ORCH-059): staging gate log — staging_status SUCCESS
Some checks failed
CI / test (push) Has been cancelled
Staging check suite passed (exit 0); C9a/C9b sandbox-infra waived (ORCH-061).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 19:20:18 +00:00
202 changed files with 16456 additions and 197 deletions

View File

@@ -12,11 +12,63 @@ ORCH_GITEA_WEBHOOK_SECRET=
ORCH_CLAUDE_BIN=/usr/bin/claude
ORCH_REPOS_DIR=/home/slin/repos
ORCH_DB_PATH=/app/data/orchestrator.db
# ORCH-042: live-tracker mode. edit (DEFAULT) -> the task card is edited in place
# (editMessageText). bump -> on every update the old card is deleted and a fresh
# one is sent silently to the BOTTOM of the chat (deleteMessage + sendMessage +
# repoint). One card per task in both modes. Any value other than "bump" -> edit.
ORCH_TRACKER_MODE=edit
# ── Agent model / effort / fallback (ORCH-41, validation ORCH-74) ─────────────
# Per-agent LLM model + reasoning effort, resolved by launcher.resolve_agent_*.
# Resolution priority (per agent): project-override (projects_json agent_models/
# agent_efforts) > ORCH_AGENT_MODEL_<AGENT> / ORCH_AGENT_EFFORT_<AGENT> >
# ORCH_AGENT_MODEL_DEFAULT / ORCH_AGENT_EFFORT_DEFAULT > CLI default (no flag).
# The frontmatter `model:` in .openclaw/agents/*.md is DESCRIPTIVE only and is NOT
# read — config below is the single source of truth for the model (ORCH-74 G1).
#
# ORCH-74 (G2): a resolved MODEL name is validated (^claude-…$ format check) before
# it reaches --model. A structurally invalid name (typo, gpt-4, empty) is logged and
# the next valid level is used (in the limit: no --model flag). Forward-compatible:
# a future claude-* version passes without editing any allowlist. EFFORT is validated
# against low|medium|high|xhigh|max (ORCH-41); an invalid effort is dropped.
#
# All 6 agents resolve to claude-opus-4-8 (model-routing G3 NOT enabled). Leave the
# per-agent overrides empty to use the default. Do NOT hardcode the model version
# anywhere except ORCH_AGENT_MODEL_DEFAULT.
ORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8
ORCH_AGENT_MODEL_ANALYST=
ORCH_AGENT_MODEL_ARCHITECT=
ORCH_AGENT_MODEL_DEVELOPER=
ORCH_AGENT_MODEL_REVIEWER=
ORCH_AGENT_MODEL_TESTER=
ORCH_AGENT_MODEL_DEPLOYER=
# Effort split: thinking agents (analyst/architect/developer/reviewer) -> high;
# mechanical agents (tester/deployer) -> medium.
ORCH_AGENT_EFFORT_DEFAULT=high
ORCH_AGENT_EFFORT_ANALYST=high
ORCH_AGENT_EFFORT_ARCHITECT=high
ORCH_AGENT_EFFORT_DEVELOPER=high
ORCH_AGENT_EFFORT_REVIEWER=high
ORCH_AGENT_EFFORT_TESTER=medium
ORCH_AGENT_EFFORT_DEPLOYER=medium
# Optional --fallback-model used when the primary is overloaded. Empty -> no flag
# (G4 NOT enabled, ADR-001 ORCH-74: determinism — all agents stay on opus-4-8). A
# non-empty value is validated by the SAME predicate as the model; a typo is dropped.
ORCH_AGENT_FALLBACK_MODEL=
# ORCH-042/ORCH-067: live-tracker mode. bump (DEFAULT since ORCH-067) -> on every
# update the old card is deleted and a fresh one is sent silently to the BOTTOM of
# the chat (deleteMessage + sendMessage + repoint), so the current status is always
# the last message in an active chat. edit -> the task card is edited in place
# (editMessageText). One card per task in both modes. Any value other than "bump"
# (incl. empty/garbage) -> edit.
ORCH_TRACKER_MODE=bump
# ORCH-067: best-effort live-overlay for the card status line. The offline core
# (stage -> Plane status, In Review from the brd-clock) always works without network;
# the overlay only fills in branches indistinguishable offline (Needs Input / Blocked /
# Rejected / Cancelled / Deploying / Monitoring after Deploy) by reading the LIVE Plane
# status with a short timeout + per-issue TTL cache. It NEVER blocks the pipeline and
# NEVER raises.
# LIVE_STATUS -> kill-switch (false -> offline core only).
# LIVE_STATUS_TTL_S -> TTL (seconds) of the per-issue live-uuid cache (hot-path guard).
# LIVE_STATUS_TIMEOUT_S -> timeout (seconds) of a single live-GET on the render path.
ORCH_TRACKER_LIVE_STATUS=true
ORCH_TRACKER_LIVE_STATUS_TTL_S=60
ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S=3
# ORCH-043: merge-gate (auto-rebase onto current origin/main + re-test + merge-lock)
# on the deploy-staging -> deploy edge. Deterministic sub-gate (no LLM) that catches
# the branch up to the CURRENT origin/main, re-tests it, and serialises merges so two
@@ -36,6 +88,43 @@ ORCH_MERGE_RETEST_TARGET=tests/
ORCH_MERGE_LOCK_TIMEOUT_S=300
ORCH_MERGE_DEFER_DELAY_S=60
ORCH_MERGE_DEFER_MAX_ATTEMPTS=5
# ORCH-026 Level A: unconditional pre-merge rebase. With the flag ON (default),
# check_branch_mergeable ALWAYS rebases the branch onto origin/main under the held
# merge-lease (not only when behind) — a deterministic structural anti-phantom on
# the scheduler edge. No-op on an up-to-date branch (rebase keeps HEAD, force-with-
# lease -> "Everything up-to-date", CI not triggered). Scope = ORCH_MERGE_GATE_REPOS.
# PREMERGE_REBASE_ALWAYS=false -> strictly pre-ORCH-026 (rebase only when behind).
ORCH_PREMERGE_REBASE_ALWAYS=true
# ORCH-026 Level B: declarative task dependencies ("B waits for A"). claim_next_job
# gates jobs whose depends-on tasks are not yet 'done' (additive job_deps table,
# NOT EXISTS) WITHOUT occupying a max_concurrency slot. Inert on an empty job_deps.
# TASK_DEPS_ENABLED=false -> claim query is 1:1 the ORCH-1 query (no gate).
# TASK_DEPS_SOURCE=db|plane|hybrid -> declaration source; db (default) never calls
# Plane on the hot path; plane/hybrid ingest Plane `blocked-by` relations and
# cache them into job_deps (the scheduler then reads only the DB).
ORCH_TASK_DEPS_ENABLED=true
ORCH_TASK_DEPS_SOURCE=db
# 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/
# force-push to main), then `done` is allowed ONLY when the deployed SHA is proven an
# ancestor of origin/main (ORCH-073 FR-1: SHA-in-main is the single criterion; a
# merged PR alone no longer confirms). A secondary regression guard then checks a
# declarative marker set (MAIN_REGRESSION_MARKERS) is still in origin/main; a missing
# marker -> alert + HOLD (NOT done), a git error of the grep itself -> fail-open.
# MERGE_VERIFY_ENABLED -> global kill-switch (false -> strictly pre-ORCH-071).
# MERGE_VERIFY_REPOS -> CSV of repos where the under-gate is REAL; empty ->
# only the self-hosting repo (orchestrator); non-self -> no-op.
# MERGE_PR_TIMEOUT_S -> per Gitea list/merge HTTP call timeout.
# MERGE_VERIFY_TIMEOUT_S -> git fetch/merge-base timeout for the ancestor + marker checks.
# 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.
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-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
@@ -117,6 +206,15 @@ ORCH_RECONCILE_GRACE_OVERRIDES_JSON=
ORCH_RECONCILE_NOTIFY_UNBLOCK=true
ORCH_RECONCILE_SKIP_BLOCKED_ENABLED=true
# ORCH-068: TTL (seconds) for the per-project Plane states cache (plane_sync
# _STATES_CACHE). Historically the cache lived for the whole process lifetime,
# so a status added to Plane after start was invisible until a restart
# ("stale set -> no pipeline action"). With a TTL the entry self-heals by
# re-fetching /states/ once it expires (reuses reload_project_states()).
# >0 -> re-fetch after this many seconds (default 300 = 5 min);
# 0 -> disable TTL -> strictly the previous lifetime cache (back-compat).
ORCH_PLANE_STATES_TTL_S=300
# ORCH-065: job-reaper + proactive merge-lease reclaim. A background daemon thread
# (src/job_reaper.py, started LAST in main.lifespan after requeue_running_jobs) reaps
# zombie 'running' jobs whose monitor/process died before writing the terminal status
@@ -190,3 +288,10 @@ ORCH_POST_DEPLOY_FAIL_THRESHOLD=3
ORCH_POST_DEPLOY_5XX_THRESHOLD=0.5
ORCH_POST_DEPLOY_AUTO_ROLLBACK=false
ORCH_POST_DEPLOY_BASE_URL=http://localhost:8500
# ── QG-0 entry validation (ORCH-069) ──────────────────────────────────────────
# Upper title-length limit for the QG-0 entry gate (_qg0_errors). The old 80-char
# cap was a hygiene limit, not structural (slug is cut to [:30] independently, the
# DB title TEXT is unbounded). Default 200. An invalid/empty value gracefully
# degrades to 200 (the process never crashes on startup).
ORCH_QG0_TITLE_MAX=200

View File

@@ -50,3 +50,6 @@ ORCH_QUEUE_POLL_INTERVAL=2.0
DEPLOY_SSH_USER=slin
DEPLOY_SSH_HOST=127.0.0.1
DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh
# QG-0 entry title-length limit (ORCH-069). Default 200; invalid/empty -> 200.
ORCH_QG0_TITLE_MAX=200

13
.gitattributes vendored Normal file
View File

@@ -0,0 +1,13 @@
# ORCH-073 (ADR-001 Р-5 / FR-4): union merge for the append-only changelog.
#
# CHANGELOG.md is append-only at the top (## [Unreleased]). Without a merge driver,
# two branches that both add an Unreleased entry collide on auto_rebase_onto_main
# (merge_gate), which rolls the branch back to `development` and can drag in stale
# neighbouring code (a phantom-merge amplifier — see ADR-001 root cause #3). The
# built-in `union` driver keeps BOTH sides' lines instead of conflicting, so both
# changelog entries survive and the branch is not rolled back.
#
# Scope is INTENTIONALLY limited to CHANGELOG.md: `union` only suits strictly
# append-only files. docs/**/*.md (README, ADR, internals) are rewritten line-by-line,
# where `union` would silently duplicate edited lines — so they are NOT included.
CHANGELOG.md merge=union

View File

@@ -1,7 +1,6 @@
---
name: analyst
description: Бизнес-аналитик. Создаёт пакет документов анализа для work item.
model: claude-sonnet-4-6
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/*)
- Bash (git log, grep — только для чтения контекста)

View File

@@ -1,7 +1,6 @@
---
name: architect
description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR.
model: claude-opus-4-7
tools:
- Filesystem (Read везде; Write только docs/)
- Bash (read-only: grep, git log)

View File

@@ -1,7 +1,6 @@
---
name: deployer
description: DevOps-агент. Запускает staging-проверку и/или прод-деплой. Пишет 15-staging-log.md и 14-deploy-log.md.
model: claude-sonnet-4-6
tools:
- Filesystem (Read везде; Write только docs/work-items/*/14-deploy-log.md, docs/work-items/*/15-staging-log.md)
- Bash (docker, git, curl, ssh)

View File

@@ -1,7 +1,6 @@
---
name: developer
description: Senior разработчик. Реализует ТЗ по ADR, пишет тесты, открывает PR.
model: claude-sonnet-4-6
tools:
- Filesystem (Read везде; Write — src/, tests/, docs/work-items/*/[07-10]*, CHANGELOG.md)
- Git (commit, push; merge запрещён)

View File

@@ -1,7 +1,6 @@
---
name: reviewer
description: Senior code reviewer. Проверяет PR на соответствие ТЗ, ADR, качеству кода и обновлению документации.
model: claude-opus-4-7
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/12-review.md)
- Git (read-only: log, diff, blame)

View File

@@ -1,7 +1,6 @@
---
name: tester
description: QA-инженер. Прогоняет тесты, оформляет отчёт.
model: claude-sonnet-4-6
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/13-test-report.md)
- Bash (pytest, curl)

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
## Стек
- Backend: FastAPI + uvicorn (Python 3.12)
- БД: SQLite (`src/db.py`)
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1)
- Агенты: 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`).
- Контейнеризация: Docker + Compose
- CI/CD: Gitea Actions (`.gitea/workflows/`)
- Деплой: docker compose на mva154
@@ -38,6 +38,25 @@ created → analysis → architecture → development → review → testing →
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3)
```
## Статусная модель Plane (ORCH-066) — индикация ≠ управление
Статусы Plane — это **слой B (индикация)**, отдельный от **слоя A (машина стадий)** `src/stages.py::STAGE_TRANSITIONS`. Plane показывает наблюдателю осмысленную картину (`Backlog → Todo → Analysis → Architecture → Development → Code-Review → Testing → Awaiting Deploy → Deploying → Monitoring after Deploy → Done` + человеческие гейты `In Review/Approved`, `Confirm Deploy`), но НИКОГДА не управляет конвейером. Маппинг и сеттеры — `src/plane_sync.py` (6 новых ключей: `to_analyse/analysis/code_review/awaiting_deploy/deploying/monitoring`), с project-relative alias-fallback: на частично сконфигурированном проекте новый ключ деградирует на базовый UUID ТОГО ЖЕ проекта (нулевая регрессия для enduro-trails). Детали — `docs/architecture/README.md`.
## Нотификации / Telegram live-tracker (ORCH-042/066/067)
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
- **Дефолт `tracker_mode``bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
Blocked / Rejected / Cancelled / Deploying / Monitoring) и **никогда не блокирует конвейер**.
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
## Конвенции
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
@@ -64,6 +83,10 @@ created → analysis → architecture → development → review → testing →
- **НЕ перезапускать / не ронять прод-контейнер** `orchestrator` в рамках задачи — встанет конвейер всех проектов.
- Любой деплой/рестарт self = групповой риск. Детали и топология — `docs/operations/INFRA.md`.
- Стадия `deploy-staging` (порт 8501) — обязательная страховка перед прод-деплоем орка.
- Прод-деплой орка запускается ТОЛЬКО переводом задачи на стадии `deploy` в выделенный
Plane-статус **«Confirm Deploy»** (ORCH-059). Статус `Approved` — человеческий гейт
конвейера и прод-деплой НЕ запускает (на `deploy` — no-op). Это разделяет «одобрить
артефакт» и «выкатить в прод», чтобы привычный approve не ронял прод случайным кликом.
---
*Паспорт проекта orchestrator. Поддерживается агентами при каждой доработке. Изолирован: описывает только этот проект (канон per-repo, см. ORCH-9).*

View File

@@ -136,6 +136,7 @@ uvicorn src.main:app --reload --port 8500
| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | Per-stage пороги, напр. `{"development":300}` | `""` |
| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | Telegram при разблокировке застрявшей задачи | `true` |
| `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED` | F-1 Guard 2 (ORCH-060): пропуск задач в Plane-статусе Blocked / Needs Input; `false` глушит только сетевой Guard 2 (Guard 1 escalated всегда активен) | `true` |
| `ORCH_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое значение → дефолт (ORCH-069) | `200` |
## Очередь задач (ORCH-1 / F-2b)

View File

@@ -9,12 +9,13 @@
- **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane.
- **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`.
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`.
- **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.
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость).
## Конвейер и Quality Gates
@@ -40,6 +41,20 @@ created → analysis → architecture → development → review → testing →
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
### Модель и эффорт по ролям (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-дефолт (без флага)**. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
| Агент | Модель | Эффорт |
|-------|--------|--------|
| analyst | claude-opus-4-8 | high |
| architect | claude-opus-4-8 | high |
| developer | claude-opus-4-8 | high |
| reviewer | claude-opus-4-8 | high |
| tester | claude-opus-4-8 | medium |
| deployer | claude-opus-4-8 | medium |
**Валидация (ORCH-74 G2, never-break):** резолвенное имя модели проходит формат-чек `is_valid_model` (`^claude-[a-z0-9.-]+$`) перед попаданием в `--model`. Невалидное (опечатка, `gpt-4`, пустое) → `logger.warning` + откат на следующий валидный уровень (в пределе — без `--model`, CLI-дефолт); мусор **никогда** не уезжает в CLI и запуск не падает. Форма — формат-чек, а не статичный allowlist: forward-compatible (будущие `claude-*` проходят без правки кода). Тот же предикат гардит inline-чтение `--fallback-model` (`agent_fallback_model` читается мимо резолва — TRZ §4). Эффорт валидируется множеством `VALID_EFFORTS` (`low|medium|high|xhigh|max`). Fallback (G4) НЕ включён (`agent_fallback_model=""`). Детали — `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`.
### Условный staging-гейт (ORCH-35)
`check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)``orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md).
@@ -58,11 +73,24 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch
Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием:
- **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`.
- **Безусловный pre-merge rebase (ORCH-026, A-2):** при `premerge_rebase_always` (дефолт `True`, скоуп `merge_gate_repos`) short-circuit `branch_is_behind_main` пропускается — `auto_rebase_onto_main` вызывается **всегда** под лизом. На актуальной ветке это no-op (`rebase` не меняет HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switch `premerge_rebase_always=False` → прежнее поведение (ребейз только при behind).
- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД.
- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. **ORCH-026 (A-1):** это окно = «merge → main-updated» (для self `done` ⇔ SHA-in-main, ORCH-073) — пока A не в `main`, B того же репо получает `merge-lock busy` → defer. Окно сериализации per-repo НЕ переписывается; кросс-репо параллелизм сохранён (лиз — per-repo файл).
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**.
Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`.
Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026).
### Зависимости задач: B ждёт A (ORCH-026, Уровень B)
Плоская очередь ORCH-1 (FIFO по `id` + `available_at` + `max_concurrency`) не выражала логических зависимостей. ORCH-026 вводит декларативные связи «задача B не стартует, пока не готовы её depends-on» — без новой стадии и без изменения `STAGE_TRANSITIONS`/`QG_CHECKS`.
- **Источник истины планировщика — БД** (аддитивная таблица `job_deps(task_id, depends_on_task_id)`): claim в горячем цикле обслуживает очередь ВСЕХ проектов и обязан быть offline-устойчив (сетевой Plane на каждый claim = встанет очередь всех проектов). Источник **декларации** настраивается `task_deps_source = db|plane|hybrid` (дефолт `db`; `plane`/`hybrid` читают Plane relations в `handle_work_item_created` и кэшируют в `job_deps`).
- **Гейт планировщика (`claim_next_job`)** — условие `NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=j.task_id AND t.stage!='done')` при `task_deps_enabled`: задача с незавершённой зависимостью **не выбирается** (агент не запускается, слот `max_concurrency` не занимается). Инертно при пустой `job_deps` → нулевая регрессия; kill-switch `task_deps_enabled=False` → запрос 1:1 как ORCH-1.
- **Детект дедлоков** — DFS-цикл-детектор (leaf `src/task_deps.py::detect_cycle`) при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert (Telegram/Plane) с перечислением цикла. Поток остальных задач не блокируется.
- **Видимость** — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (`update_task_tracker`, never-raise); Plane `Blocked` — на дедлоке (не на нормальном коротком ожидании, чтобы не флаппить). Инвариант «одна карточка на задачу» сохранён.
- **Совместимость:** `reconciler` F-1 пропускает dep-заблокированные задачи (`is_task_ready`, паттерн ORCH-060); `reaper` сканирует только `running` → dep-блок остаётся `queued`, не трогается. Зависимости — только intra-repo (v1).
- **Наблюдаемость:** блок `task_deps` в `GET /queue` (заблокированные задачи, держатель merge-lease, defer-счётчики, обнаруженные циклы) — read-only.
Подробнее: [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`.
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
@@ -70,21 +98,25 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch
а `deploy_status: SUCCESS` означает доказанный health-ok, не декларацию LLM. Три фазы
(детерминированно, без LLM в критическом пути self-restart):
- **Фаза A (вход в `deploy`)** — при `deploy_require_manual_approve=true` вместо запуска
прод-deployer выставляется approval-pending статус Plane + запрос approve
(Plane-коммент + Telegram). Перехват в `advance_stage` ПОСЛЕ `check_staging_status`
и merge-gate.
- **Фаза B (Plane → `Approved`)** — `advance_stage(deploy, finished_agent=None)`
прод-deployer выставляется approval-pending статус Plane + запрос перевести задачу
в статус **«Confirm Deploy»** (ORCH-059; Plane-коммент + Telegram). Перехват в
`advance_stage` ПОСЛЕ `check_staging_status` и merge-gate.
- **Фаза B (Plane → `Confirm Deploy`, ORCH-059)** —
`advance_stage(deploy, finished_agent=None, confirm_deploy=True)`
запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами +
build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**;
маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет).
Обычный `Approved` на `deploy` (`confirm_deploy=False`) — детерминированный no-op
(не деплоит и не откатывает).
- **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result`
(exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`,
вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты:
`SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
Approve = смена статуса Plane на `Approved` (status-only verdict model; комментарии
не управляют конвейером). На старте — обязательный ручной approve (флаг `true`); полный
авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
Триггер прод-деплоя = смена статуса Plane на `Confirm Deploy` (ORCH-059; status-only
verdict model; комментарии не управляют конвейером). `Approved` остаётся исключительно
человеческим гейтом конвейера и прод-деплой не запускает. На старте — обязательный
ручной approve (флаг `true`); полный авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
прочие репо — прежний синхронный ssh-деплой агентом. Контракты не меняются:
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status`/`_parse_deploy_status`, БАГ-8,
terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe состояние —
@@ -92,6 +124,88 @@ sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без мигр
Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально —
`docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано)
Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на
`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy`
— привычный жест approve молча запускал прод-рестарт (групповой self-hosting
риск). ORCH-059 разделяет жесты: вводится отдельный логический статус
`confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на
`deploy`; `Approved` остаётся исключительно гейтом конвейера.
- `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в
`_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) →
**fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`).
- `handle_issue_updated` маршрутизирует `Confirm Deploy``handle_confirm_deploy`
(гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`.
- `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B
(`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при
`confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status`
не запускается → нет ложного отката БАГ-8).
- CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved».
- Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`,
`QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений.
- Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша
состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`).
Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md`
(уточняет/триггер Фазы B относительно adr-0007).
#### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge)
**Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`):
на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main`
исторически делал ТОЛЬКО он → детерминированный путь
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага
merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done`
достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей
ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и
теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит
**детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`**
(симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting:
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит
**ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper —
все идут через `advance_stage`), закрывая дыру обхода merge.
- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером
нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его
не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3).
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (idempotency no-op повтор) → иначе
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Выбор PR строго по `head.ref==branch`
И `base.ref=="main"`. Никогда push/force-push в `main`.
- **Верификатор `merge_gate.verify_merged_to_main` (семантика ORCH-073, FR-1):** подтверждение —
**ТОЛЬКО** `git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision`
якорь ORCH-058). PR-флаг `pr_already_merged` **больше НЕ подтверждает merge** (удалён из verify):
он понижен до idempotency-guard `merge_pr` и засчитывает merged PR лишь при `head.ref==branch`
И `base.ref=="main"` (исключает авто docs-PR). Пустой SHA / git-ошибка → `False` (fail-closed),
never-raise.
- **Регресс-гард целостности `main` (ORCH-073, FR-5):** `merge_gate.check_main_regression` в
`_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main`
содержит декларативный набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`,
`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → alert «main regressed» +
HOLD (НЕ `done`, ALERT-only). Fail-open на git-ошибке грепа (регресс — только при `count==0`).
Kill-switch `regression_guard_enabled`; non-self → no-op. Набор — append-only константа,
значимая задача дописывает свой маркер.
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть
инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy →
done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут).
- **Защита от CHANGELOG-затирания (ORCH-073, FR-4):** корневой `.gitattributes` с
`CHANGELOG.md merge=union` → правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main`
без конфликта, ветка не откатывается в `development` и не тащит устаревший код-сосед. `docs/**`
под union НЕ ставится (union только для append-only).
- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) +
`merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`.
never-raise; идемпотентность по **SHA-в-main** (INV-4, не «любой merged PR»); ручной approve
сохранён (`Confirm Deploy`).
- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр
`QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД,
БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**.
Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема).
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md) +
[adr-0014](adr/adr-0014-merge-verify-sha-source-of-truth.md) (amends 0013 — SHA-в-main как
единственный критерий + регресс-гард, ORCH-073); детально —
`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`.
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 —
@@ -206,11 +320,21 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
retry-count проверяется первым (дёшево, локальный SQL).
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
**ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane
(`state.group ∈ {completed, cancelled}`, fallback — логические ключи
`done`/`cancelled`) исключаются из actionable-выборки per-issue — проектно-независимо,
устойчиво к UUID-алиасингу после переименований статусов (ORCH-066); (2) `_note_unblock`
(лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО при **подтверждённом state change**
(сравнение стадии задачи до/после `_dispatch`; no-op dispatch → тишина), плюс in-memory
дедуп по `issue_id→state`. Восстанавливает инвариант silence-when-in-sync (AC-9/AC-10).
Детали — `docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`.
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
development-задаче repo; неоднозначность → не резолвим).
- **F-4 observability:** при разблокировке — лог-строка `reconciler: <wi> <stage>
разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок
состояния в `GET /queue` (блок `reconcile`).
состояния в `GET /queue` (блок `reconcile`). **ORCH-068** добавляет в снимок
счётчики `skipped_terminal_total` (исключённые терминалы) и `deduped_total`
(подавленные повторные нотификации).
Реализация: `src/reconciler.py` (daemon-поток по образцу `queue_worker`), стартует в
`main.lifespan` **после** `worker.start()`, останавливается в `finally` **перед**
@@ -281,6 +405,46 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц
Подробнее: [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md), детально —
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`.
### Осмысленная статусная модель Plane (ORCH-066 — реализовано)
Plane-доска была семантически перегружена: `In Progress` означал «человек запускает
конвейер», «идёт анализ», «идёт прод-деплой» и «возврат из Needs Input» одновременно.
ORCH-066 наводит порядок по утверждённой Owner модели, меняя **только слой B**
(Plane-индикация: `src/plane_sync.py` + точки простановки в `src/stage_engine.py`/
`src/webhooks/plane.py`/`src/reconciler.py`) и **не трогая слой A** (`STAGE_TRANSITIONS`,
инвариант). Статус — индикация, не управление (вердикты по-прежнему из YAML-frontmatter):
```
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
Monitoring after Deploy → Done
```
`[...]` = человеческий вход-триггер; остальное ставит орк.
- **6 новых логических ключей** (`to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/
`deploying`/`monitoring`) в `_PLANE_NAME_TO_KEY` (резолв по имени) + `_DEFAULT_STATES`.
`To Analyse` заменяет `In Progress` как вход-триггер (старт + resume аналитика из Needs
Input; fork «старт vs resume» по `get_task_by_plane_id`+`has_active_job_for_task` —
сохранён). Стадии: analysis→`Analysis`, review→`Code-Review` (`_STAGE_TO_STATE_KEY`).
- **Self-deploy фазы:** Phase A → `Awaiting Deploy` (разгружает `In Review`), Phase B →
`Deploying`, Phase C/terminal-sync (self) → `Monitoring after Deploy` (НЕ `Done` сразу);
post-deploy monitor (ORCH-021): HEALTHY-окно → `Done`, DEGRADED → `Blocked` (тик
по-прежнему НИКОГДА не рестартит прод — ALERT_ONLY). Не-self репо: `deploy → Done` как
сейчас (terminal-sync разводится по `post_deploy.post_deploy_applies`).
- **Fail-closed (project-relative alias-fallback):** отсутствующий новый статус в проекте
деградирует на **собственный базовый UUID того же проекта** (`to_analyse/analysis→in_progress`,
`code_review→review`, `awaiting_deploy→in_review`, `deploying→in_progress`,
`monitoring→done`) — индикация откатывается к текущей, конвейер не ломается, PATCH валиден
даже при частичной конфигурации. Enduro (статусы не создаются) → строго прежнее поведение.
Усиленный паттерн ORCH-059 AC-7.
- **Reconciler:** F-2 триггер `in_progress`→`to_analyse`; Guard 2 skip-set расширен
активными ожиданиями (`awaiting_deploy`/`deploying`/`monitoring`) с **вычитанием базовых
рабочих статусов** — на enduro (алиасы схлопнуты) нулевой регресс, на orchestrator skip
реальных ожиданий (BR-13).
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, exit-коды хука,
merge-gate, `Confirm Deploy`, механизм `Needs Input` (analyst-only), схема БД — без
изменений. Без нового kill-switch (раскат гейтится созданием Plane-статусов оператором).
Инфра-предусловие — `docs/work-items/ORCH-066/07-infra-requirements.md`.
Подробнее: `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`.
## Откаты
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
@@ -315,6 +479,7 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц
- `tasks` — задачи и их стадии
- `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»
## Изоляция (git worktree, ORCH-2)
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
@@ -339,3 +504,6 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц
---
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).*
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-059 (выделенный статус-триггер прод-деплоя «Confirm Deploy», ADR `docs/work-items/ORCH-059/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-059 (маппинг `"Confirm Deploy"→"confirm_deploy"` в src/plane_sync.py `_PLANE_NAME_TO_KEY`, НЕ в `_DEFAULT_STATES` = fail-closed; ветка `handle_confirm_deploy` + fail-closed `.get("confirm_deploy")` в src/webhooks/plane.py `handle_issue_updated`; keyword-only `confirm_deploy` в src/stage_engine.py `advance_stage` — Фаза B деплоит ТОЛЬКО при `confirm_deploy=True`, иначе `Approved`-на-`deploy` = no-op; CTA Фазы A просит «Confirm Deploy»; эксплуатация — статус доски «Confirm Deploy» в Plane-проекте ORCH, `docs/work-items/ORCH-059/07-infra-requirements.md`).*
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-066 (осмысленная статусная модель Plane — слой B, `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`) — реализовано в ветке feature/ORCH-066-plane (только Plane-индикация: новые ключи `to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/`deploying`/`monitoring` в `_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES` + project-relative `_STATE_ALIAS_FALLBACK` в get_project_states + `_STAGE_TO_STATE_KEY` analysis/review + 5 новых `set_issue_*` в src/plane_sync.py; триггер `in_progress`→`to_analyse` и `set_issue_analysis` в src/webhooks/plane.py; Phase A→Awaiting Deploy / Phase B→Deploying / terminal-sync split monitoring↔done / post-deploy monitor HEALTHY→Done DEGRADED→Blocked в src/stage_engine.py; F-2 триггер `to_analyse` + Guard 2 skip-set с вычитанием base_working в src/reconciler.py; `STAGE_TRANSITIONS`/QG/схема БД НЕ трогаются; без kill-switch — раскат гейтится созданием 6 Plane-статусов оператором, `docs/work-items/ORCH-066/07-infra-requirements.md`; обновлять при изменении этих мест).*
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-068 (livelock-fix reconciler F-2: терминал-исключение по группе состояния + `_note_unblock` только при подтверждённом state change + дедуп; TTL `_STATES_CACHE`, `docs/work-items/ORCH-068/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-068 (D1 терминал-гард по группе `_is_terminal_state` + `get_project_state_groups` в src/plane_sync.py; D2 сравнение стадии до/после `_dispatch` + дедуп-словарь в src/reconciler.py; TTL-запись `_STATES_CACHE` + флаг `plane_states_ttl_s` в src/config.py; счётчики `skipped_terminal_total`/`deduped_total` в `/queue`; обновлять также при изменении src/reconciler.py F-2, src/plane_sync.py `get_project_states`/`get_project_state_groups`/`_STATES_CACHE`).*

View File

@@ -17,11 +17,16 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
| adr-0012 | Security-гейт (secrets/deps) | accepted | 2026-06-08 | ORCH-022 |
| 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 |
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
> свободный номер (текущий максимум — `0011`).
> свободный номер (текущий максимум — `0015`).
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
## Формат
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.

View File

@@ -69,6 +69,15 @@ grace + `max_concurrency=1`); never-raise на единицу работы; ти
задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise,
тишина при пропуске).
- **ORCH-068** (`docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`):
фикс livelock F-2 (спам `_note_unblock` по синхронизированной done-задаче после
ORCH-066). F-2 исключает терминалы по **группе состояния** (`completed`/`cancelled`,
fallback — ключи `done`/`cancelled`) проектно-независимо; `_note_unblock` — только при
подтверждённом state change (сравнение стадии до/после `_dispatch`) + in-memory дедуп;
`_STATES_CACHE` получает TTL (`ORCH_PLANE_STATES_TTL_S`, дефолт 300с, `0`=lifetime).
Инварианты adr-0007 сохранены (источник истины — Plane; реестры/схема/`handle_*`/F-1/F-3
не меняются; never-raise; kill-switch'и).
## Связи
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра

View File

@@ -0,0 +1,63 @@
# adr-0013: Merge-в-main + пост-деплой верификация как условие `done` (фикс фантомного merge)
- **Статус:** accepted
- **Дата:** 2026-06-08
- **Задача:** ORCH-071 (CRITICAL bug)
- **Детальный ADR:** `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
## Контекст
Для self-hosting репо `orchestrator` стадия `deploy` идёт детерминированным путём
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`), а LLM-агент
`deployer` НЕ запускается. Фактический merge PR в `main` исторически делал **только**
агент `deployer` → на self-hosting пути **нет шага merge-в-main вообще**. Detached
host-деплой лишь retag'ает образ + рестартит 8500; `done` достигается по
`deploy_status: SUCCESS` без верификации `main`. «Зелёный» деплой (образ из рабочей
ветки) маскирует отсутствие merge → следующая задача срезает ветку от устаревшего `main`
и теряет код предшественника. Накопительно потеряны ORCH-022/059/066/068. Вторичный
фактор: Phase B рестартит прод → merge внутри живого процесса гонялся бы с рестартом
(урок №3).
## Решение
Детерминированный **merge-актор + пост-merge верификация** как **под-гейт ребра
`deploy → done`**, врезанный в единственную функцию перехода `advance_stage` (симметрично
edge-под-гейтам security/merge-gate/image-freshness). `STAGE_TRANSITIONS`,
`check_deploy_status`/`_parse_deploy_status`, реестр `QG_CHECKS`, схема БД — **не меняются**.
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`).
Гейтит **ВСЕ** пути к `done` единообразно: `run_deploy_finalizer` (Phase C), reconciler
F-1, job-reaper — все идут через `advance_stage`. Закрывает дыру: reconciler F-1 иначе
протолкнул бы `done` в обход merge.
- **Merge в Phase C (после рестарта), НЕ в Phase B.** Phase C finalizer —
restart-surviving (reserved-job `deploy-finalizer`, claim воркером нового контейнера,
re-drive reaper'ом). Merge физически строго ПОСЛЕ рестарта → рестарт его не убивает
(G3 вторым вариантом — «шаг, переживающий рестарт»).
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) →
иначе Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в
`main`. never-raise.
- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ
`git merge-base --is-ancestor <validated_sha> origin/main`. never-raise → `False`
(«не подтверждено»).
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged
есть инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено →
штатный `deploy → done` (терминал-sync / post-deploy monitor как сегодня) +
`merged_to_main: true` во frontmatter `14-deploy-log.md` (наблюдаемость, `deploy_status:`
нетронут).
- **Идемпотентность (INV-5):** `pr_already_merged` перед merge; verify зелёный для
уже-слитого PR; повтор без дубль-merge/ложного отката.
- **Условность (как ORCH-35/43/58):** `merge_verify_enabled` (kill-switch, дефолт `true`) +
`merge_verify_repos` (пусто → только self-hosting). Non-self репо — no-op, merge остаётся
за агентом `deployer`.
## Инварианты
never-raise на verify/merge (ошибка → alert, не падение конвейера); не рестартить/не ронять
прод 8500; ручной approve прод-деплоя сохранён (`Confirm Deploy`, ORCH-059); только PR-merge
API Gitea; restart-safe (sentinel + jobs, без миграции БД).
## Последствия
Невозможно «`done` + прод задеплоен, а PR `open`». Минусы: при недоступной Gitea verify
консервативно `False` → возможен ложный HOLD+alert (снимается повтором; fail-closed для
`done` приоритетен); HOLD требует ручного вмешательства. Диагностика фантома — runbook
`docs/operations/PHANTOM_MERGE_RUNBOOK.md` (G4).

View File

@@ -0,0 +1,77 @@
# adr-0014: SHA-в-main — единственный критерий merge-verify + регресс-гард целостности `main`
- **Статус:** accepted
- **Дата:** 2026-06-08
- **Задача:** ORCH-073 (BUG CRITICAL — эрозия `main`)
- **Amends:** [adr-0013](adr-0013-merge-verify-gate.md) (ORCH-071) — меняет КРИТЕРИЙ подтверждения merge.
- **Детальный ADR:** `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
## Контекст
adr-0013 (ORCH-071) ввёл под-гейт merge-verify на ребре `deploy → done`, но допускал
подтверждение merge по **ИЛИ-критерию**: `verify_merged_to_main` возвращал `True`, если
`pr_already_merged(repo, branch)` **ЛИБО** SHA — предок `origin/main`. `pr_already_merged`
засчитывал **любой** merged PR ветки, включая авто docs-PR (staging/deploy-логи). У одной
feature-ветки в `main` сливались только docs-PR, а code-PR — нет → `pr_already_merged`=`True`
verify `CONFIRMED``done`, хотя кода в `main` не было. Накопительно потеряны ORCH-067 (ссылки
`plane_issue_link`) и ORCH-069 (`qg0_title_max`). Вторичный усилитель — CHANGELOG-ребейзы,
откатывающие ветку и тащащие устаревший код-сосед. Восстановление кода (G1) выполнено вручную
restore-PR #76; этот ADR устраняет корень навсегда.
## Решение
1. **SHA-в-main — единственный критерий (FR-1).** `verify_merged_to_main(repo, branch, sha)`
подтверждает merge **ТОЛЬКО** прямым фактом `git merge-base --is-ancestor <sha> origin/main`
(после `git fetch origin main`). OR-ветка `pr_already_merged` **удалена** из верификатора.
Пустой `sha` / любая git-ошибка → `False` (fail-closed: alert + HOLD). never-raise (INV-1).
2. **`pr_already_merged` → idempotency-guard, различающий code-PR/docs-PR (FR-2).** Засчитывает
merged PR только при `head.ref==<feature-branch>` И `base.ref=="main"` (явный фильтр в цикле,
не ненадёжный query-параметр `head`). Используется лишь как защита `merge_pr` от второго merge,
НЕ как подтверждение `done`.
3. **`merge_pr` сливает именно code-ветку (FR-3).** Выбор открытого PR по `head.ref==branch` И
`base.ref=="main"`; merge только Gitea `POST /pulls/{index}/merge`, никогда push/force-push в
`main`. Источник истины «слилось» — FR-1.
4. **Регресс-гард целостности `main` (FR-5).** Новая `merge_gate.check_main_regression`,
вызываемая в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done`: проверяет, что
`origin/main` содержит **декларативный набор маркеров** ключевых функций ранее-merged задач
(`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → **alert «main
regressed» + HOLD** (НЕ `done`, БЕЗ авто-отката на `development` — инфра-дефект, ALERT-only как
ORCH-021/071). Набор — append-only константа `MAIN_REGRESSION_MARKERS` в `merge_gate.py`
(расширяется каждой значимой задачей). **Fail-open** на git-ошибке самого грепа (регресс
утверждается только при детерминированном `count==0`); первичный фейл-клозед — SHA-в-main.
Kill-switch `regression_guard_enabled` (дефолт `true`); non-self → no-op.
5. **`.gitattributes CHANGELOG.md merge=union` (FR-4).** В корне репо; авто-слияние правок
`## [Unreleased]` без конфликта → `auto_rebase_onto_main` не откатывает ветку и не тащит
устаревший код-сосед. `docs/**/*.md` под union **НЕ** ставится (union только для append-only;
доки переписываются построчно).
## Инварианты
never-raise на verify/merge/регресс-гарде (ошибка → alert/HOLD, не падение); прод 8500 не
рестартится/не падает в рамках merge; merge только Gitea PR-API без force-push в `main`; ручной
`Confirm Deploy` (ORCH-059) сохранён; идемпотентность по «SHA-в-main», а не по «любому merged PR»;
non-self репо (enduro) — merge/verify/регресс-гард без изменений. `STAGE_TRANSITIONS`, реестр
`QG_CHECKS`, `check_deploy_status`, схема БД, внешние HTTP-эндпоинты — **без изменений**.
## Альтернативы
- Сохранить PR-флаг как со-критерий verify (с фильтром head/base) — отклонено: PR можно слить и
тут же откатить ребейзом-соседом; надёжен только факт «SHA в main».
- `docs/**/*.md merge=union` — отклонено: тихая дубликация строк в переписываемых доках.
- Регресс-гард с авто-откатом / хранением маркеров в БД/Plane — отклонено (Не-цель «не менять
схему БД/Plane»; реакция ALERT-only).
- Fail-closed на marker-grep — отклонено: ложный HOLD при git-сбое; marker-grep вторичен.
## Последствия
Невозможно «`done` + прод задеплоен, а code-PR не в `main`». Ложно-зелёный по docs-PR устранён в
корне. CHANGELOG-конфликты больше не откатывают ветку. Регресс соседнего кода ловится отдельным
гардом. Минус: при недоступной Gitea/git verify консервативно `False` → возможен ложный HOLD+alert
(снимается повтором; fail-closed для `done` приоритетен). Набор маркеров требует дисциплины —
значимая задача дописывает свой маркер.
## Связи
- Amends adr-0013 (ORCH-071), наследует adr-0006 (merge-gate), adr-0011 (job-reaper/lease).
- Детально: `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.

View File

@@ -0,0 +1,47 @@
# adr-0015: Зависимости задач + сериализация merge внутри репо
**Статус:** accepted · **Дата:** 2026-06-08 · **Источник:** ORCH-026
**Связи:** дополняет adr-0006 (merge-gate), adr-0011 (merge-lease + reclaim), adr-0013/0014
(merge-verify, SHA-in-main), adr-0002 (очередь). Детально —
`docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`.
## Контекст
Эрозия `main` 08.06 родилась из некоординированного параллелизма задач одного репо (ветки от
устаревшего `main`, фантом-merge затирает соседа). adr-0014 закрыл последствия; ORCH-026 — корень
на уровне планировщика. Плюс исходный скоуп ORCH-026: декларативные зависимости задач (B ждёт A).
## Решение
**Уровень A — сериализация merge/деплоя (per-repo).** Окно сериализации уже обеспечивается
merge-lease (adr-0011): захват в `check_branch_mergeable`, удержание до release (PR-merged webhook /
`deploy→done`=SHA-in-main для self / откат / проактивный reclaim). Это и есть окно
«merge → main-updated» — **механизм не переписывается**. Добавляется единственное новое поведение:
**безусловный proactive pre-merge rebase** (флаг `premerge_rebase_always`, дефолт `True`, скоуп
`merge_gate_repos`): под лизом всегда вызывается `auto_rebase_onto_main` (no-op + «Everything
up-to-date» на актуальной ветке → CI не триггерится; реальный догон на отстающей). Инвариант:
никаких push в `main`, force только `--force-with-lease` на ветку.
**Уровень B — декларативные зависимости.** Аддитивная таблица `job_deps(task_id,
depends_on_task_id)`**источник истины планировщика** (offline-устойчивость: сетевой Plane в
горячем claim встанет очередью всех проектов). Источник декларации настраивается
`task_deps_source = db|plane|hybrid` (дефолт `db`); планировщик всегда читает БД-кэш. Гейт —
условие `NOT EXISTS` в `claim_next_job` (задача не выбирается, пока есть незавершённая зависимость;
слот `max_concurrency` не занимается). Циклы — DFS-детектор (`src/task_deps.py`) + `set_issue_blocked`
+ alert. Видимость — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (Plane Blocked — на дедлоке).
Зависимости — только intra-repo (v1).
## Альтернативы
Отдельный merge-lock/merge-queue (дублирует adr-0011); расширение release-точек лиза (не нужно —
окно уже корректно); Plane как источник истины планировщика (self-hosting risk); гейт зависимостей
в воркере с claim+requeue (churn vs. чистый `NOT EXISTS`); поле в `tasks` вместо таблицы (M:N хуже).
## Последствия
Минимально-инвазивно: `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты (паттерн врезки), переиспользует
merge-gate/merge-lease целиком. Обе фичи инертны без данных → нулевая регрессия для enduro-trails.
restart-safe, never-raise, kill-switch на каждую (`premerge_rebase_always`, `task_deps_enabled`).
Миграция — только аддитивная (`CREATE TABLE/INDEX IF NOT EXISTS`). Ограничение: B v1 — intra-repo.
Self-hosting safety: изменения идут через `deploy-staging``Confirm Deploy`, без внеочередного
рестарта прода.

View File

@@ -111,12 +111,12 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
Вместо ~15 отдельных сообщений на задачу оркестратор держит **ОДНУ** live-карточку на задачу (`update_task_tracker`), которая обновляется на каждом переходе стадии. Текст рендерится статически из БД (`render_task_tracker`: стадии, токены, стоимость, BRD-подтверждение, итоги). Карточка всегда тихая (`disable_notification=True`); отдельные пинги шлют только `notify_approve_requested` / `notify_error`. `message_id` хранится в `tasks.tracker_message_id`; helpers `get_tracker_message_id` / `set_tracker_message_id`. Контракт всего компонента — **never raises**.
**Режимы (ORCH-042, `ORCH_TRACKER_MODE` → `Settings.tracker_mode`).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` нулевая регрессия и безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
**Режимы (ORCH-042, `ORCH_TRACKER_MODE` → `Settings.tracker_mode`; дефолт переключён `edit → bump` в ORCH-067).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` → безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
| Режим | Поведение при обновлении |
|-------|--------------------------|
| `edit` (дефолт) | первый вызов → `send_telegram` (тихо) + сохранение `message_id`; далее → `edit_telegram` на сохранённый id. Новое сообщение шлётся ТОЛЬКО при `EDIT_GONE` (удалено/старше 48ч/невалидный id). `EDIT_NOT_MODIFIED` / `EDIT_FAILED` → нового сообщения нет (анти-дубль). |
| `bump` | карточка пересоздаётся внизу чата: best-effort `delete_telegram(старый_id)``send_telegram(text, disable_notification=True)``set_tracker_message_id(new_id)` **только** при успешном send (`new_mid is not None`). За один вызов — не более одного нового сообщения. |
| `bump` (дефолт, ORCH-067) | карточка пересоздаётся внизу чата: best-effort `delete_telegram(старый_id)``send_telegram(text, disable_notification=True)``set_tracker_message_id(new_id)` **только** при успешном send (`new_mid is not None`). За один вызов — не более одного нового сообщения. Живая карточка всегда «догоняет» переписку. |
| `edit` | первый вызов → `send_telegram` (тихо) + сохранение `message_id`; далее`edit_telegram` на сохранённый id. Новое сообщение шлётся ТОЛЬКО при `EDIT_GONE` (удалено/старше 48ч/невалидный id). `EDIT_NOT_MODIFIED` / `EDIT_FAILED` → нового сообщения нет (анти-дубль). |
**`delete_telegram(message_id) -> bool`** (low-level, never raises). Семантика возврата — «исчезло ли старое сообщение»:
- `ok:true``True`;
@@ -128,6 +128,12 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
**Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются.
**Строка Plane-статуса и кликабельный номер (ORCH-067, слой B — индикация).** Под заголовком карточка несёт строку `📍 <Plane-статус>` по модели ORCH-066. Источник — двухслойный, контракт **never raises**:
- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy→⏸ Awaiting Deploy`, `done→Done`) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). Неизвестная/битая стадия → безопасный дефолт `To Analyse`.
- **Live-overlay** `_live_plane_branch_override` — best-effort: дорисовывает ветви-статусы, неразличимые оффлайн (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy), чтением живого Plane-статуса (`fetch_issue_state` с коротким `tracker_live_status_timeout_s`, TTL-кэш `tracker_live_status_ttl_s`, kill-switch `tracker_live_status`). Любой сбой / выключенный флаг / нехватка данных → оффлайн-метка; `⏸️ In Review` (авторитет brd-часов) overlay не консультирует. Анти-false-positive: `deploying/monitoring`, алиасящие базовый UUID на проекте без выделенного статуса (enduro), не вызывают override.
**Кликабельный номер задачи (ORCH-067).** Номер в заголовке карточки И во всех уведомлениях орка, где упоминается `work_item_id`, — HTML-ссылка на issue в Plane через общий `plane_issue_link` / `link_for` (URL строит `_plane_issue_url` с loopback/workspace/project-гардами, переиспользуя резолв ORCH-017). Fail-safe: при нехватке любого из (web-base/не-loopback, workspace, project_id, plane_issue_id) → `html.escape(work_item_id)` без `<a>`; динамические части экранируются, `<a>`-разметка валидна под `parse_mode=HTML`. Алерты `stage_engine`/`launcher`/`security_gate`/`reconciler` переведены на `link_for` (резолвит `repo`+`plane_issue_id` из БД по `task_id` или `work_item_id`).
## Database Schema
```sql

View File

@@ -0,0 +1,33 @@
# Lessons Learned — 2026-06-08: статус `Confirm Deploy` не триггерит Phase B (мёртвый триггер)
## Контекст
ORCH-066 ввела новую статусную модель Plane, включая человекочитаемый статус **`Confirm Deploy`** для прод-деплойного approve-gate (self-deploy Phase B). Орк сам выставляет задачу в `Awaiting Deploy` / `Confirm Deploy` через `set_issue_awaiting_deploy()` и т.п.
## Инцидент (2026-06-08, первый реальный прод-self-deploy — ORCH-068)
Слава нажал статус **`Confirm Deploy`** в Plane, ожидая запуск прод-деплоя. Орк ответил `no pipeline action` и НИЧЕГО не запустил. Прод-деплой стартовал только после ручного перевода в **`Approved`**.
## Root cause
Диспетчер статусов `handle_issue_status` (`src/webhooks/plane.py` ~158-166) слушает РОВНО три состояния:
```python
if new_state == proj_states["to_analyse"]: await handle_status_start(...)
elif new_state == proj_states["approved"]: await handle_verdict(..., approved=True)
elif new_state == proj_states["rejected"]: await handle_verdict(..., approved=False)
else: logger.info("... no pipeline action")
```
Phase B (прод-деплой) триггерится в `_try_advance_stage` (`src/stage_engine.py` ~215-224) при `current_stage == "deploy" and finished_agent is None` — то есть ТОЛЬКО когда пришёл вебхук `Approved`. Статус `Confirm Deploy` в эту тройку НЕ входит → ветка `else` → no-op.
**ORCH-066 добавила статус как МЕТКУ (запись), но не подключила обратный путь (чтение/триггер).** Классическая дыра: протестировали, что орк правильно СТАВИТ статус, но не протестировали, что нажатие этого статуса человеком РЕАЛЬНО запускает действие.
## Почему не поймали тестирование/ревью
1. **Не в scope ORCH-068.** ORCH-068 чинит reconciler (BRD §6 N1-N3 явно: не трогать диспетчер статусов / Phase B). Тестер прогнал TC-01..13 — все про reconciler/terminal-статусы. Ревьюер смотрел diff reconciler.py/plane_sync.py. Корректно — это дефект ORCH-066, не 068.
2. **Дыра ORCH-066.** Её тесты, видимо, проверяли запись статусов, а не обратный триггер.
3. **Staging не покрывает прод-путь.** Phase A (staging-деплой) автоматический, ручной `Confirm Deploy` живёт ТОЛЬКО на прод-пути, который на staging не гоняется. Поэтому всплыло лишь на первом реальном прод-деплое.
## Уроки
1. **Тестировать обратный путь статусов, не только запись.** Для каждого статуса, который человек может нажать, нужен тест «нажатие → ожидаемое pipeline-действие». Запись (орк ставит статус) и чтение (орк реагирует на статус) — два разных контракта.
2. **Прод-only пути (ручной Confirm Deploy) нуждаются в явном тесте/чеклисте.** Staging их не ловит by design. Любой approve-gate, доступный человеку, обязан иметь регресс-тест на триггер.
3. **Новый статус = подключить В ОБЕ стороны.** При добавлении статуса в модель — сразу проверить, что диспетчер `handle_issue_status` его слушает (если он actionable), а не только что орк его выставляет.
4. **UX-консистентность:** статус, названный действием («Confirm Deploy»), обязан выполнять это действие. Иначе оператор жмёт интуитивную кнопку, а система молчит → потеря доверия к автономности.
## Фикс
Заведена ORCH-070: подключить `Confirm Deploy` (или его actionable-эквивалент) к триггеру Phase B в `handle_issue_status`, + регресс-тест на обратный путь статусов прод-деплоя. Source-of-truth и существующий `Approved`-путь не ломать (обратная совместимость).

View File

@@ -0,0 +1,47 @@
# Lessons Learned — 2026-06-08: «Фантомный merge» — прод деплоится, но код не сливается в main
## Severity: CRITICAL (потеря целостности main, накопительная потеря кода между задачами)
## Резюме
Self-deploy (Phase B) собирал прод-образ из ВЕТКИ задачи и рапортовал `finalize SUCCESS` + `post-deploy HEALTHY`, но git-merge ветки в `main` НЕ происходил. PR оставался `open`. Следующая задача срезала свою ветку от устаревшего main → теряла код незалитых предшественников. Накопительно потеряны в main: **ORCH-022, ORCH-059, ORCH-066, ORCH-068** (PR#67/68/69/70 — все open, merged=False). Последний реально слитый — ORCH-065 (PR#66).
## Как обнаружено
Симптом: ORCH-067 переведён в `To Analyse`, но конвейер не стартовал (`no pipeline action`). Причина — прод слушал старый триггер `in_progress`, а не `to_analyse` (ORCH-066). При разборе выяснилось: код ORCH-066 не в проде, хотя он «деплоился».
Решающее наблюдение оператора (Слава): «спам ET-002 начался СРАЗУ после деплоя 66 → значит код деплоился». Это вскрыло механизм: код 66 БЫЛ в проде 22:1705:32, потом стёрт деплоем 068 (срезан от старого main без 66).
## Доказательная база (как подтверждали — воспроизводимый метод)
1. **PR-статус (Gitea API):** PR#67(022)/68(059)/69(066)/70(068) = open, merged=False. PR#66(065) = merged=True (последний честный).
2. **md5-сверка файлов прод vs origin/main vs ветка:**
- `src/reconciler.py`, `src/plane_sync.py`: prod md5 == ветка ORCH-068 != main → прод = снимок ветки 068, НЕ main.
- `src/webhooks/plane.py`: prod == main == ветка-068 (ветка 068 этот файл не трогала → видна старая база без to_analyse).
3. **git merge-base:** ветка ORCH-068 срезана от `bb03350` (ORCH-065), не от кода 066. История ветки-068 по 066 содержит только `docs staging`, кода (`to_analyse`) нет.
4. **Таймлайн логов:** деплой 22:17 (ветка-066, сломанный reconciler) → спам ET-002 начался; деплой 05:32 (ветка-068, база 065 без 66) → спам прекратился (0 после 05:33). Подтверждает: прод-образ = снимок ВЕТКИ, меняется при каждом деплое, теряет незалитое.
## Root cause (гипотеза → нужен код-аудит self_deploy/merge_gate)
Self-deploy Phase B инициирует прод-деплой из worktree ветки (BUILD-ONCE из validated commit). Шаг git-merge ветки в main:
- ЛИБО не вызывается на self-hosting пути (Phase B уходит в detached host-процесс, finalizer пишет SUCCESS-маркеры, но merge отдельно и молча скипается/падает),
- ЛИБО регресс фикса ORCH-065 (idempotent merge / merge-lease reclaim): guard `pr_already_merged` или lease-reclaim ошибочно считает PR уже слитым / не докатывает merge после рестарта контейнера (а Phase B ИМЕННО рестартит контейнер → процесс, державший merge-lease, умирает до завершения merge).
Симптоматически ORCH-065 был последним успешным merge — деградация началась СРАЗУ после него или из-за взаимодействия его механики с self-deploy-рестартом.
## Почему конвейер не заметил
- `finalize SUCCESS` и `post-deploy HEALTHY` маркеры пишутся НЕЗАВИСИМО от факта merge. Пайплайн считает задачу done по этим маркерам, git-состояние main не верифицируется.
- Прод здоров (образ из ветки рабочий) → health-check зелёный → нет сигнала о проблеме.
- Дыра видна только при сравнении main с прод ИЛИ когда следующая задача теряет код предыдущей (что и случилось с 67).
## Уроки
1. **Деплой ОБЯЗАН верифицировать, что код реально в main ПОСЛЕ деплоя.** finalize SUCCESS без проверки `git merge-base origin/main == deployed_commit` (или PR.merged==true) — фальшивый зелёный. Добавить post-merge верификацию: deployed SHA должен быть предком origin/main.
2. **Маркер «deployed» != «merged».** Нельзя считать задачу завершённой по staging/post-deploy-маркерам, если PR не закрыт merge. Гейт: задача → done ТОЛЬКО при PR.merged==true.
3. **Self-deploy рестартит контейнер → любой держатель merge-lease/незавершённый git-шаг умирает.** Merge ДОЛЖЕН завершиться и быть подтверждён ДО рестарта прод-контейнера, либо merge выносится в шаг, переживающий рестарт (как requeue_running_jobs, но для merge-в-main).
4. **Срез ветки от main делает целостность main критичной.** Если main отстаёт — каждая новая задача наследует дыру. main = единственный источник для новых веток, его рассинхрон с прод = накопительная потеря.
5. **Метод диагностики (сохранить как runbook):** при подозрении на рассинхрон — (a) Gitea API PR list merged-флаги, (b) md5 prod-файлов vs `git show origin/main:<file>`, (c) merge-base ветки vs main, (d) таймлайн деплой-логов. Эти 4 проверки однозначно локализуют фантом.
## Действия
- Восстановление main: интеграционная ветка `integ/restore-main-2026-06-08` — последовательный merge 022→059→066→068 (docs union-resolved, reconciler-конфликт 066⊕068 разрешён: каркас 068 livelock-fix + триггер to_analyse 066), полный pytest, затем merge в main + передеплой.
- Заведён критбаг ORCH-071: «фантомный merge — self-deploy без верификации merge в main» (root-fix: post-deploy verify + done-гейт по PR.merged + merge до рестарта).
- ORCH-070 (Confirm Deploy trigger) частично ДУБЛИРУЕТ ORCH-059 (handle_confirm_deploy уже написан в 059) — после долива 059 пересмотреть scope 070 (остаётся только display-слой статусов Monitoring after Deploy).
## Связанные
- ORCH-065 (последний честный merge; подозрение на регресс его merge-механики)
- ORCH-066/068 (потерянный код), ORCH-059 (Confirm Deploy trigger, тоже потерян)
- Урок 2026-06-08 confirm-deploy-deadtrigger (симптом того же корня)

View File

@@ -0,0 +1,125 @@
# Runbook — диагностика «фантомного merge» (ORCH-071)
> **Когда применять.** Задача дошла до `done` (или прод задеплоен «зелёным»), но есть
> подозрение, что её ветка **не влита в `main`** — следующая задача срежет ветку от
> устаревшего `main` и потеряет код предшественника (постмортем
> `docs/history/LESSONS_2026-06-08_phantom-merge.md`). Этот runbook даёт 4 проверки
> для **однозначной локализации** фантома.
С ORCH-071 такой исход блокируется автоматически: под-гейт `deploy → done`
(`stage_engine._handle_merge_verify`) сначала **детерминированно вливает PR**
(`merge_gate.merge_pr`, Gitea PR-merge API), затем **верифицирует merge**
(`merge_gate.verify_merged_to_main`) и НЕ пускает задачу в `done`, пока merge не
подтверждён (alert + HOLD). Этот runbook — для ручной перепроверки/инцидентов
(в т.ч. при выключенном kill-switch `ORCH_MERGE_VERIFY_ENABLED=false`).
Подставьте значения:
```bash
OWNER=admin # settings.gitea_owner
REPO=orchestrator # репозиторий
BRANCH=feature/ORCH-071-slug # ветка задачи
GITEA=http://localhost:3000 # settings.gitea_url
TOKEN=<gitea_token> # settings.gitea_token
FILE=src/stage_engine.py # любой файл, гарантированно изменённый задачей
```
---
## Проверка 1 — Gitea API: список PR + флаги `merged`
Показывает, считает ли сам Gitea PR влитым.
```bash
curl -s -H "Authorization: token $TOKEN" \
"$GITEA/api/v1/repos/$OWNER/$REPO/pulls?state=all" \
| python3 -c 'import sys,json; \
[print(p["number"], p["state"], "merged="+str(p.get("merged")), p["head"]["ref"]) \
for p in json.load(sys.stdin)]'
```
* **Фантом НЕ подтверждён (всё хорошо):** строка ветки `$BRANCH` имеет `merged=True`.
* **Фантом подтверждён (по этому критерию):** PR ветки `state=open` / `merged=False`
(или PR отсутствует), при том что задача в `done` / прод задеплоен.
---
## Проверка 2 — md5 прод-файлов vs `git show origin/main:<file>`
Сверяет содержимое файла на проде с тем, что лежит в `origin/main`.
```bash
# в прод-контейнере (или через docker exec orchestrator):
md5sum "/app/$FILE"
# содержимое того же файла из origin/main (на хосте, в клоне репо):
git -C /home/slin/repos/$REPO fetch origin main -q
git -C /home/slin/repos/$REPO show "origin/main:$FILE" | md5sum
```
* **Совпало:** прод соответствует `main` (фантома нет ИЛИ задача не меняла этот файл —
возьмите файл из проверки 3/diff'а ветки).
* **Разошлось:** прод собран из ветки, а `main` его не получил → косвенный признак фантома.
---
## Проверка 3 — `git merge-base` ветки vs `main`
Главный детерминированный критерий: является ли HEAD ветки предком `origin/main`.
```bash
git -C /home/slin/repos/$REPO fetch origin -q
SHA=$(git -C /home/slin/repos/$REPO rev-parse "origin/$BRANCH")
git -C /home/slin/repos/$REPO merge-base --is-ancestor "$SHA" origin/main \
&& echo "MERGED: ветка влита в main" \
|| echo "NOT MERGED: ветка НЕ предок origin/main (ФАНТОМ)"
```
Это ровно та проверка, что выполняет `merge_gate.verify_merged_to_main` (rc=0 → влито).
* **`MERGED`:** фантома нет.
* **`NOT MERGED`:** фантом подтверждён — `main` не содержит коммитов задачи.
---
## Проверка 4 — таймлайн деплой-логов
Восстанавливает порядок событий: был ли merge до/после деплоя, и был ли он вообще.
```bash
# Вердикт деплоя + новое поле merge-верификации (ORCH-071):
git -C /home/slin/repos/$REPO show "origin/$BRANCH:docs/work-items/<WI>/14-deploy-log.md" \
| sed -n '1,12p' # frontmatter: deploy_status:, merged_to_main:
# Наблюдаемость под-гейта в живом сервисе:
curl -s "$GITEA_HEALTH/queue" | python3 -c \
'import sys,json; print(json.load(sys.stdin)["merge_verify"])'
# -> {"enabled":..., "merge_verified_total":..., "not_merged_alerts_total":..., "last_alert_wi":...}
# Журнал хоста по деплою (sentinel-каталог задачи):
ls -la /home/slin/repos/.deploy-state-$REPO/<WI>/
cat /home/slin/repos/.deploy-state-$REPO/<WI>/hook.log
```
* `deploy_status: SUCCESS` + `merged_to_main: false` → деплой прошёл, merge — нет
(это и есть класс ORCH-071; задача должна быть удержана на `deploy`, не `done`).
* `not_merged_alerts_total` растёт / `last_alert_wi == <WI>` → под-гейт уже поднял alert.
---
## Критерий «фантом подтверждён»
Фантомный merge считается **подтверждённым**, если выполняется ХОТЯ БЫ ОДНО из:
1. Проверка 1: PR ветки `state=open` / `merged=False` (или PR нет), а задача в `done`.
2. Проверка 3: `merge-base --is-ancestor` вернул **NOT MERGED** (HEAD ветки не предок `origin/main`).
3. Проверка 4: `14-deploy-log.md` имеет `deploy_status: SUCCESS` при `merged_to_main: false`.
Проверка 2 — вспомогательная (зависит от того, менял ли файл задачей), используется
для подтверждения проверок 1/3.
### Что делать при подтверждённом фантоме
1. **Влить PR вручную** через Gitea (PR-merge API / UI) — НИКОГДА не `git push`/`--force` в `main` (INV-4).
2. Повторить approve задачи (re-drive) — под-гейт переоценит: merge подтвердится → задача уйдёт в `done`.
3. Если фантом случился при выключенном kill-switch — включить `ORCH_MERGE_VERIFY_ENABLED=true`.

View File

@@ -0,0 +1,7 @@
# Business Request: Управление зависимостями задач (B ждёт A) в очереди
Work Item ID: ORCH-026
## Description
TBD

View File

@@ -0,0 +1,135 @@
# 01-BRD — Управление зависимостями задач (B ждёт A) в очереди
**Work Item:** ORCH-026
**Repo:** orchestrator (self-hosting)
**Branch:** feature/ORCH-026-b-a
**Стадия:** analysis
**Источник:** предложение Стрим, одобрено Славой (2026-06-04); дополнение Слава+Стрим 2026-06-08 (инцидент эрозии `main`)
---
## 1. Контекст и проблема
### 1.1 Первопричина (мотивация СЕЙЧАС — инцидент 08.06)
Эрозия `main` 08.06 (потеря кода ORCH-067/069, фантом-merge) родилась НЕ из логических
зависимостей, а из **некоординированного параллелизма**: несколько self-hosting задач
(ORCH-067/069/071) одновременно срезали ветки от `main` и правили общие файлы
(`CHANGELOG.md`, `notifications.py`, `config.py`). Последствия:
- CHANGELOG-конфликты на `auto_rebase` → откаты `deploy-staging → development` (дорого:
ORCH-069 = 3 попытки = $3.98);
- тихое затирание кода соседа при merge ветки, срезанной от устаревшего `main` (фантом).
**ORCH-073** закрыл ПОСЛЕДСТВИЯ (3 рубежа: CHANGELOG `merge=union` + SHA-in-main verify +
регресс-гард маркеров). ORCH-026 должен закрыть **ПЕРВОПРИЧИНУ**: задачи одного репо не
должны мешать друг другу в `main`.
### 1.2 Исходный скоуп (плоская очередь ORCH-1)
Очередь (`src/queue_worker.py`, ORCH-1) — плоская: `jobs` упорядочены по `id` (FIFO),
гейтятся только `available_at` и `max_concurrency`. Нельзя выразить «задача B не стартует,
пока не готова A». Декомпозиция эпиков (ORCH-025) порождает заведомо зависимые подзадачи.
### 1.3 Что уже есть (опора, НЕ переписывать)
- **ORCH-1** — персистентная очередь (`jobs`), atomic claim, `available_at`-defer, restart-safe.
- **ORCH-065** — `merge-lease` (`src/merge_gate.py`): per-repo файловый лиз
`.merge-lease-<repo>.json`, неблокирующий acquire, holder-aware release, проактивный
реклейм мёртвого/устаревшего держателя. **Сейчас лиз держится только на ребре
`deploy-staging → deploy`** (от merge-gate до фактического merge).
- **ORCH-043** — merge-gate: `branch_is_behind_main`, `auto_rebase_onto_main` (rebase
**только когда ветка отстаёт или при конфликте**), `retest_branch`.
- **ORCH-073** — merge-verify: `verify_merged_to_main` (SHA-in-main), `check_main_regression`.
- **Plane-статусы** `Blocked` / `Needs Input` + `set_issue_blocked` (`src/plane_sync.py`).
- **Telegram live-tracker** (`src/notifications.py`) — одна карточка на задачу, уже умеет
показывать статус `Blocked`.
---
## 2. Цель (бизнес-результат)
Задачи одного репозитория перестают повреждать `main` друг друга, а очередь умеет
выражать логические зависимости между задачами — БЕЗ потери параллелизма между разными
репозиториями и без риска для self-hosting прода.
---
## 3. Два уровня требований (объединить в одной задаче; приоритет — Уровень A)
### Уровень A — Сериализация merge/деплоя внутри ОДНОГО репо (КРИТИЧНО, корень эрозии)
Закрывает первопричину инцидента 08.06.
- **A-1.** В рамках ОДНОГО репо merge-в-`main` + деплой должны быть **сериализованы**: пока
задача A не слита в `main` (и для self-hosting — не задеплоена), задача B того же репо НЕ
доходит до своего merge/деплоя от устаревшего `main`.
- **A-2.** B перед своим merge-gate **обязана ребейзнуться на СВЕЖИЙ `main`** (где уже есть
A) — **proactive pre-merge rebase**, а не только при текстовом конфликте (как сейчас в
ORCH-043). Цель: B всегда несёт актуальный код предшественников → структурный анти-фантом
на уровне планировщика (дополняет рубежи ORCH-073, не заменяет).
- **A-3.** Сериализация — **только внутри одного репо**. Задачи РАЗНЫХ репо (orchestrator vs
enduro-trails) параллелятся свободно (общая БД/очередь — пропускная способность не падает).
- **A-4.** Механизм — минимально-инвазивный и **restart-safe** (как ORCH-1/065): переживает
рестарт прод-контейнера, не оставляет навсегда захваченных ресурсов (опора на проактивный
реклейм ORCH-065).
- **A-5.** **Совместимость с self-hosting safety:** не ронять/не рестартить прод-контейнер
вне штатного deploy; гейт `Confirm Deploy` (ORCH-059) сохранён; никаких push/force-push в
`main`.
- **A-6.** Защита от взаимоблокировки: B при занятой сериализации **defer** (повторная
постановка с задержкой через `available_at`), а НЕ откат на `development` и НЕ вечное
ожидание; bounded defer-бюджет (анти-livelock, как `merge_defer_max_attempts`).
### Уровень B — Декларативные зависимости (исходный скоуп ORCH-26)
- **B-1.** Задача может объявить связь `blocked-by` / `blocks` (depends-on).
- **B-2.** Планировщик очереди (ORCH-1) **не запускает** заблокированную задачу, пока все её
depends-on не достигли терминального состояния (`done`).
- **B-3.** **Защита от дедлоков:** циклические зависимости детектируются; задача в цикле не
«пропадает молча» — выставляется `Blocked` + alert (Telegram/Plane).
- **B-4.** **Видимость:** заблокированная задача видна — Plane-статус `Blocked` и/или
ожидание в Telegram-карточке (что и кого ждёт).
---
## 4. Открытые вопросы для архитектора (НЕ решаются на этапе анализа)
> Аналитик фиксирует требования; выбор механизма — за архитектором (ADR в `06-adr/`).
1. **Где хранить связи (Уровень B):** Plane relations (родное, видимо в UI, но требует
сетевого запроса и зависит от Plane) vs таблица в БД (`job_deps`/поля `tasks`, надёжно и
offline, но дубль источника) vs **гибрид** (Plane — источник декларации, БД — кэш для
планировщика). Рекомендация анализа: гибрид с offline-fallback (см. §6).
2. **Механизм сериализации (Уровень A):** глобальный per-repo merge-lock vs FIFO merge-queue
vs **обязательный pre-merge rebase + расширение окна merge-lease** (от «момента merge» до
«main-updated»). Выбрать минимально-инвазивный, restart-safe, переиспользующий ORCH-065/043.
3. **Граница окна сериализации для self-hosting:** для не-self репо «merged в main» = конец
окна; для self (orchestrator) деплой асинхронный (Phase B/C, ORCH-036/071) — нужно решить,
до какого события держать лиз (до `merged_to_main: true` / до `done`).
4. **Совместимость B и A:** depends-on (B) на уровне постановки в очередь vs merge-сериализация
(A) на уровне merge-gate — разные точки конвейера; убедиться, что не конфликтуют.
---
## 5. Вне скоупа (Non-goals)
- Изменение машины стадий `STAGE_TRANSITIONS` (сериализация/зависимости — врезки/гейты, не
новые стадии — паттерн ORCH-043/058/071).
- Приоритизация/перепланирование задач по весам (только зависимости и сериализация).
- Кросс-репо зависимости (A-3 явно запрещает кросс-репо сериализацию; кросс-репо логические
зависимости — возможный follow-up, не v1).
- Отмена/замена рубежей ORCH-073 — ORCH-026 их **дополняет** на уровне планировщика.
---
## 6. Заинтересованные стороны
- **Owner (Слава)** — одобряет BRD; держатель self-hosting прод-риска.
- **Стрим** — автор предложения.
- **Конвейер агентов** — потребитель: developer/deployer работают с веткой, которую затрагивает
сериализация; reviewer проверяет обновление доки.
---
## 7. Критерии успеха (бизнес-уровень)
- Две зелёные задачи одного репо больше не способны затереть код друг друга в `main` на уровне
планировщика (без участия рубежей-последствий ORCH-073).
- Задача может объявить зависимость; заблокированная задача не стартует раньше времени и видна
наблюдателю.
- Пропускная способность разных репо не деградирует.
- Прод-контейнер orchestrator не падает и не рестартится вне штатного `Confirm Deploy`.
Точные PASS/FAIL — `03-acceptance-criteria.md`.

View File

@@ -0,0 +1,134 @@
# 02-ТЗ — Управление зависимостями задач (B ждёт A) в очереди
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** analysis
> ТЗ фиксирует ТРЕБОВАНИЯ к изменениям (модули, контракты, артефакты). Конкретный механизм
> сериализации и место хранения связей — решение архитектора (ADR в `06-adr/`); ниже отмечены
> как «КАНДИДАТ / решает архитектор». Аналитик не предлагает архитектуру.
---
## 1. Задействованные модули `src/`
| Модуль | Роль в задаче | Уровень |
|--------|---------------|---------|
| `src/queue_worker.py` | Планировщик: `_drain_once` / `claim_next_job` — точка учёта зависимостей и сериализации при выборе job. | A + B |
| `src/db.py` | Очередь `jobs` / `tasks`; `claim_next_job`, `enqueue_job`, `count_running_jobs`. Кандидат на хранение связей и блокировки claim. | A + B |
| `src/merge_gate.py` | merge-lease (ORCH-065), `branch_is_behind_main` / `auto_rebase_onto_main` (ORCH-043) — опора для proactive pre-merge rebase и расширения окна сериализации. | A |
| `src/qg/checks.py` | `check_branch_mergeable` (под-гейт ребра `deploy-staging → deploy`) — точка форсированного pre-merge rebase. | A |
| `src/stage_engine.py` | `advance_stage` — врезки гейтов; точка интеграции сериализации/верификации. | A |
| `src/webhooks/plane.py` | `handle_work_item_created` / `start_pipeline` — приём задачи; точка чтения relations (если источник — Plane). | B |
| `src/plane_sync.py` | `set_issue_blocked`, `get_project_states` (`blocked`/`needs_input`), relations API. | B |
| `src/notifications.py` | live-карточка: индикация `Blocked` / «ждёт ORCH-NNN». | B |
| `src/config.py` | Новые kill-switch + scope-настройки (паттерн `*_enabled` / `*_repos`). | A + B |
| `src/reconciler.py` / `src/job_reaper.py` | Не ломать: skip заблокированных задач (как уже делается для Blocked/Needs-Input, ORCH-060/068); реклейм ресурсов сериализации. | A + B |
---
## 2. Требования к изменениям — Уровень A (сериализация merge/деплоя)
### 2.1 Proactive pre-merge rebase (A-2)
- На ребре `deploy-staging → deploy`, ДО фактического merge (в составе `check_branch_mergeable`
или соседнего под-гейта), ветка задачи **всегда** догоняется на свежий `origin/main`
**не только при `branch_is_behind_main`/конфликте**.
- Переиспользовать `merge_gate.auto_rebase_onto_main` (rebase + `push --force-with-lease`
ТОЛЬКО ветки задачи). Текстовый конфликт → существующий контракт: `rebase --abort` → откат на
`development` (как ORCH-043).
- **Инвариант:** никаких push/force-push в `main`.
### 2.2 Расширение окна merge-lease (A-1, A-3, A-4)
- **КАНДИДАТ (решает архитектор):** держать per-repo merge-lease (ORCH-065) не только «на
момент merge», а на окно **«merge → main-updated»** (для self — до подтверждения
`merged_to_main: true` / `done`), чтобы B не дошла до своего merge, пока A не в `main`.
- Acquire — **неблокирующий** (как сейчас): занято → **defer** задачи B через
`enqueue_job(available_at_delay_s=...)`, bounded бюджет (анти-livelock; ср.
`merge_defer_max_attempts`). Откат на `development` НЕ применять для defer.
- Release — holder-aware (как `release_merge_lease`), на merged-вебхуке / `deploy→done` /
откате / по проактивному реклейму (ORCH-065 `reclaim_stale_lease`).
- Сериализация **строго per-repo** (`.merge-lease-<repo>.json`) — кросс-репо параллелизм не
затрагивается (A-3).
### 2.3 Условность и безопасность (A-5)
- Реально только для применимых репо: kill-switch + CSV-scope (паттерн `merge_gate_repos` /
`merge_verify_repos`; пусто → только self-hosting `orchestrator`).
- `STAGE_TRANSITIONS`, `Confirm Deploy` (ORCH-059), exit-коды deploy-хука, БАГ-8,
terminal-sync — **без изменений**.
- Контракт **never-raise** для всех новых функций (как соседи в `merge_gate.py`).
---
## 3. Требования к изменениям — Уровень B (декларативные зависимости)
### 3.1 Декларация связи (B-1)
- **КАНДИДАТ хранения (решает архитектор, см. BRD §4.1):**
- вариант Plane relations: читать `blocked-by` через Plane API в `handle_work_item_created`;
- вариант БД: новая таблица `job_deps(task_id, depends_on_task_id)` или поле в `tasks`
(idempotent `_ensure_column` миграция, как ORCH-065 `jobs.pid`);
- гибрид: Plane — декларация, БД — кэш для планировщика (offline-устойчивость).
- Миграция БД (если выбран вариант с таблицей/колонкой) — **только аддитивная**
(`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), безопасная на живой прод-БД с общими
данными enduro-trails.
### 3.2 Гейт планировщика (B-2)
- При выборе job (`claim_next_job` / `_drain_once`) задача с незавершёнными depends-on
**не клеймится** (аналог `available_at`-gate): пропускается до тех пор, пока все depends-on
не `done`. Не должна занимать слот `max_concurrency`.
- Реализация — **leaf-функция** с чистой логикой «готова ли задача к запуску» (тестируемо
юнитами, never-raise), по образцу `staging_verdict.py` / `post_deploy.py`.
### 3.3 Защита от дедлоков (B-3)
- Детектор циклов в графе depends-on (DFS/обнаружение цикла) — чистая функция, юнит-тестируемая.
- Цикл → задача(и) НЕ запускается молча: `set_issue_blocked` + alert (Telegram/Plane) с
указанием цикла. Не блокировать поток других задач.
### 3.4 Видимость (B-4)
- Заблокированная задача: Plane-статус `Blocked` (`set_issue_blocked`) и/или строка ожидания в
Telegram-карточке («⏳ ждёт ORCH-NNN»). Использовать существующий механизм карточки
(`notifications.update_task_tracker`), контракт never-raise / silent.
- `reconciler` F-1 уже пропускает Blocked/Needs-Input (ORCH-060/068) — убедиться, что новые
заблокированные-по-зависимости задачи тоже пропускаются (не «разблокируются» ошибочно).
---
## 4. Изменения API (endpoints)
- **Новые HTTP endpoints не требуются.**
- **Наблюдаемость:** расширить снимок `GET /queue` блоком о зависимостях/сериализации
(по образцу блоков `reconcile` / `reaper` / `post_deploy` / `merge_verify`): кол-во
заблокированных задач, держатель merge-lease, defer-счётчики, обнаруженные циклы. Read-only,
никогда не источник истины для решений.
## 5. Изменения схемы БД
- **КАНДИДАТ (если выбран БД/гибрид для Уровня B):** аддитивная таблица `job_deps` или колонка
в `tasks` (см. §3.1). Только `CREATE TABLE IF NOT EXISTS` / `_ensure_column`. Без изменения
существующих колонок `jobs`/`tasks`. Restart-safe, безопасно на общей прод-БД.
- Уровень A (сериализация) — **без изменения схемы БД** (merge-lease файловый, как ORCH-065).
## 6. Требования к новым QG checks
- **Новый зарегистрированный QG-чек НЕ вводится** (паттерн ORCH-071/058: под-гейт — врезка в
`advance_stage` или расширение `check_branch_mergeable`, а не новая запись в `QG_CHECKS`).
- Реестр `QG_CHECKS` — без изменений.
## 7. Конфигурация (`src/config.py`)
Новые настройки по паттерну `*_enabled` (kill-switch) + `*_repos` (CSV scope, пусто →
self-hosting). КАНДИДАТ-имена (финализирует архитектор):
- Уровень A: `merge_serialize_enabled` / `merge_serialize_repos` (или расширение
`merge_gate_*`); опционально `premerge_rebase_always` (вкл proactive rebase).
- Уровень B: `task_deps_enabled` / `task_deps_source` (`plane|db|hybrid`).
Дефолты — обратная совместимость (для не-self репо — прежнее поведение).
## 8. Артефакты pipeline (создать/обновить В ТОМ ЖЕ PR)
- `06-adr/ADR-001-*.md` — решение по сериализации (A) и хранению зависимостей (B).
- Обновить `docs/architecture/README.md` (раздел про очередь/merge-gate/сериализацию).
- Обновить `CLAUDE.md` (паспорт: конвейер/инварианты, если меняется поведение очереди).
- Обновить `CHANGELOG.md` (`## [Unreleased]`).
- Если вводится таблица БД — отразить в `08-data-requirements.md` (создаёт архитектор).
- `07-infra-requirements.md` — если требуется новый Plane-статус/настройка relations.
## 9. Инварианты (НЕ нарушать)
1. `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`check_staging_status`,
`Confirm Deploy` (ORCH-059), БАГ-8, terminal-sync — без изменений.
2. Никаких push/force-push в `main`; force только `--force-with-lease` на ветку задачи.
3. Сериализация — строго per-repo; кросс-репо параллелизм сохранён.
4. never-raise во всех новых функциях; restart-safe состояние.
5. ORCH-026 дополняет рубежи ORCH-073, не заменяет.
6. Прод-контейнер orchestrator не рестартится вне штатного `Confirm Deploy`.

View File

@@ -0,0 +1,107 @@
# 03-Критерии приёмки — ORCH-026
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** analysis
Каждый критерий — проверяемое условие PASS/FAIL. Маппинг на тесты — `04-test-plan.yaml`.
---
## Уровень A — Сериализация merge/деплоя внутри одного репо
### AC-A1 — Сериализация merge внутри репо
- **PASS:** пока задача A применимого репо удерживает окно merge (merge-lease не освобождён /
`main` ещё не обновлён), задача B того же репо НЕ доходит до фактического merge — она
**defer**-ится (повторная постановка через `available_at`), а не мержится от устаревшего `main`.
- **FAIL:** B мержится/деплоится, пока A не в `main`; или B откатывается на `development` вместо
defer.
### AC-A2 — Proactive pre-merge rebase
- **PASS:** перед merge ветка задачи **всегда** догоняется на свежий `origin/main` (вызывается
rebase), даже когда текстового конфликта нет и ветка формально не «behind» по старой проверке;
после rebase ветка содержит код предшественника (A).
- **FAIL:** rebase запускается только при конфликте/`branch_is_behind_main`, и B мержится без
кода A.
### AC-A3 — Кросс-репо параллелизм сохранён
- **PASS:** задача в `orchestrator` и задача в `enduro-trails` доходят до merge/деплоя
параллельно — сериализация одного репо не блокирует другой (lease/гейт строго per-repo).
- **FAIL:** задача одного репо ждёт освобождения ресурса, удерживаемого задачей ДРУГОГО репо.
### AC-A4 — Restart-safe
- **PASS:** после рестарта прод-контейнера состояние сериализации восстанавливается корректно;
мёртвый держатель merge-lease проактивно реклеймится (ORCH-065), конвейер не встаёт навсегда.
- **FAIL:** рестарт оставляет навсегда захваченный lease → конвейер всех проектов встаёт.
### AC-A5 — Self-hosting safety
- **PASS:** прод-контейнер orchestrator НЕ рестартится/не падает вне штатного `Confirm Deploy`
(ORCH-059); нет push/force-push в `main`; `STAGE_TRANSITIONS` и реестр `QG_CHECKS` не изменены.
- **FAIL:** любой незапрошенный рестарт прода, прямой push в `main`, или изменение машины стадий.
### AC-A6 — Anti-deadlock / anti-livelock при defer
- **PASS:** при занятой сериализации B defer-ится с задержкой и bounded бюджетом; исчерпание
бюджета → эскалация (alert/Blocked), не бесконечный цикл и не откат.
- **FAIL:** B уходит в вечный defer-цикл, либо немедленно откатывается на `development`.
### AC-A7 — Условность (не-self репо без регресса)
- **PASS:** при выключенном kill-switch и для репо вне scope поведение конвейера 1:1 как до
ORCH-026 (нулевая регрессия для enduro-trails).
- **FAIL:** не-self репо меняет поведение merge/деплоя.
---
## Уровень B — Декларативные зависимости
### AC-B1 — Декларация зависимости
- **PASS:** задача может объявить `blocked-by`/`depends-on` (через выбранный источник —
Plane relations / БД / гибрid), и связь корректно считывается планировщиком.
- **FAIL:** связь не считывается / теряется.
### AC-B2 — Гейт планировщика (B не стартует до A)
- **PASS:** задача с незавершённым depends-on **не клеймится** воркером (не запускается агент,
слот `max_concurrency` не занимается), пока все depends-on не достигли `done`; как только A
становится `done` — B становится claimable.
- **FAIL:** B запускается раньше завершения A; или занимает слот, простаивая.
### AC-B3 — Детект дедлоков (циклы)
- **PASS:** циклическая зависимость (A→B→A и длиннее) детектируется детерминированно; задача(и)
в цикле → `Blocked` + alert (Telegram/Plane) с указанием цикла; поток остальных задач не
блокируется.
- **FAIL:** цикл приводит к молчаливому вечному ожиданию или к падению воркера.
### AC-B4 — Видимость заблокированной задачи
- **PASS:** заблокированная задача видна — Plane-статус `Blocked` и/или строка ожидания в
Telegram-карточке (что/кого ждёт); инвариант «одна карточка на задачу» сохранён.
- **FAIL:** заблокированная задача невидима наблюдателю.
### AC-B5 — Совместимость с reconciler/reaper
- **PASS:** `reconciler` F-1 НЕ «разблокирует» задачу, заблокированную по зависимости (как уже
делает для Blocked/Needs-Input, ORCH-060/068); reaper не реапит корректно ожидающую задачу.
- **FAIL:** reconciler продвигает заблокированную задачу мимо её depends-on.
---
## Общие (оба уровня)
### AC-G1 — never-raise
- **PASS:** любая ошибка (git/сеть/БД/Plane) в новой логике не пробрасывается в `advance_stage`/
воркер; деградирует консервативно (defer/skip/fail-closed), конвейер не падает.
- **FAIL:** необработанное исключение роняет воркер/монитор-поток.
### AC-G2 — Kill-switch
- **PASS:** глобальный kill-switch выключает фичу целиком → поведение 1:1 как до ORCH-026.
- **FAIL:** при выключенном флаге поведение изменено.
### AC-G3 — Документация обновлена (golden source)
- **PASS:** в ТОМ ЖЕ PR обновлены `docs/architecture/README.md`, `CLAUDE.md` (если изменилось
поведение очереди), `CHANGELOG.md`, заведён ADR в `06-adr/`. Reviewer проверяет.
- **FAIL:** код изменён, документация — нет (→ REQUEST_CHANGES).
### AC-G4 — Миграция БД безопасна (если применимо)
- **PASS:** миграция только аддитивная (`CREATE TABLE IF NOT EXISTS`/`_ensure_column`),
идемпотентна, безопасна на живой общей прод-БД; существующие данные enduro-trails не затронуты.
- **FAIL:** деструктивная миграция / изменение существующих колонок.
### AC-G5 — Тесты зелёные
- **PASS:** новые unit+integration тесты (`04-test-plan.yaml`) проходят; существующий
`pytest tests/ -q` остаётся зелёным (нет регресса merge-gate/merge-verify/reconciler/reaper).
- **FAIL:** красный pytest или регресс существующих тестов.

View File

@@ -0,0 +1,169 @@
work_item: ORCH-026
description: >
План тестов для управления зависимостями задач (Уровень B) и сериализации
merge/деплоя внутри одного репо (Уровень A). Стек: pytest. Имена модулей/функций —
кандидаты; финализирует архитектор/разработчик. Все новые функции — never-raise.
tests:
# ---------------- Уровень A: сериализация merge/деплоя ----------------
- id: TC-A01
type: unit
description: >
Proactive pre-merge rebase: ветка догоняется на свежий origin/main ДАЖЕ когда
branch_is_behind_main вернул бы False (нет конфликта). Проверить, что rebase
вызывается всегда перед merge (AC-A2).
module: tests/test_orch026_premerge_rebase.py
expected: PASS
- id: TC-A02
type: unit
description: >
Расширенное окно merge-lease: пока A держит lease (окно merge→main-updated),
acquire для B того же репо возвращает busy → defer (не откат). holder-aware
release не удаляет чужой lease (AC-A1, AC-A6).
module: tests/test_orch026_merge_serialize.py
expected: PASS
- id: TC-A03
type: unit
description: >
Сериализация строго per-repo: lease/гейт orchestrator не влияет на задачу
enduro-trails — обе claimable параллельно (AC-A3).
module: tests/test_orch026_merge_serialize.py
expected: PASS
- id: TC-A04
type: unit
description: >
Restart-safe + проактивный реклейм: мёртвый держатель lease (pid не жив)
реклеймится reclaim_stale_lease; конвейер не встаёт навсегда (AC-A4).
module: tests/test_orch026_merge_serialize.py
expected: PASS
- id: TC-A05
type: unit
description: >
Anti-livelock defer: B defer-ится с available_at-задержкой и bounded бюджетом;
исчерпание → эскалация (Blocked/alert), не бесконечный цикл (AC-A6).
module: tests/test_orch026_merge_serialize.py
expected: PASS
- id: TC-A06
type: unit
description: >
Условность/kill-switch: при выключенном флаге и для репо вне scope поведение
merge/деплоя 1:1 как до ORCH-026 — no-op (AC-A7, AC-G2).
module: tests/test_orch026_conditionality.py
expected: PASS
- id: TC-A07
type: unit
description: >
Self-hosting safety: новая логика никогда не делает push/force-push в main;
force только --force-with-lease на ветку задачи; STAGE_TRANSITIONS не изменены
(AC-A5).
module: tests/test_orch026_conditionality.py
expected: PASS
- id: TC-A08
type: integration
description: >
Сквозной сценарий: две задачи одного репо проходят deploy-staging→deploy; B не
доходит до merge, пока A не в main; после A→done B ребейзится на свежий main
(несёт код A) и мержится. main не теряет код A (AC-A1/AC-A2).
module: tests/test_orch026_serialize_integration.py
expected: PASS
# ---------------- Уровень B: декларативные зависимости ----------------
- id: TC-B01
type: unit
description: >
Чтение/декларация связи blocked-by из выбранного источника (Plane/БД/гибрид);
связь корректно резолвится в depends_on_task_id (AC-B1). never-raise при
недоступности источника → консервативно (нет связи или fail-closed по решению ADR).
module: tests/test_orch026_task_deps.py
expected: PASS
- id: TC-B02
type: unit
description: >
Гейт готовности (leaf-функция): задача с незавершённым depends-on НЕ ready;
все depends-on в done → ready. Чистая логика, юнит-тестируемая (AC-B2).
module: tests/test_orch026_task_deps.py
expected: PASS
- id: TC-B03
type: unit
description: >
Детект циклов: A→B→A (и длиннее) детектируется детерминированно; ацикличный
граф → циклов нет. Чистая функция (AC-B3).
module: tests/test_orch026_dep_cycles.py
expected: PASS
- id: TC-B04
type: unit
description: >
Цикл → set_issue_blocked + alert (Telegram/Plane), без падения воркера и без
блокировки потока других задач (AC-B3, AC-G1).
module: tests/test_orch026_dep_cycles.py
expected: PASS
- id: TC-B05
type: unit
description: >
claim_next_job не клеймит заблокированную задачу (не занимает слот
max_concurrency); как только depends-on done — задача становится claimable (AC-B2).
module: tests/test_orch026_task_deps.py
expected: PASS
- id: TC-B06
type: unit
description: >
Видимость: заблокированная задача отражается в Plane-статусе Blocked и/или
строке ожидания Telegram-карточки; инвариант «одна карточка на задачу» сохранён
(AC-B4). notifications never-raise / silent.
module: tests/test_orch026_dep_visibility.py
expected: PASS
- id: TC-B07
type: unit
description: >
reconciler F-1 НЕ разблокирует задачу, заблокированную по зависимости (как для
Blocked/Needs-Input); reaper не реапит корректно ожидающую (AC-B5).
module: tests/test_orch026_task_deps.py
expected: PASS
- id: TC-B08
type: integration
description: >
Сквозной сценарий: B объявлена blocked-by A; при постановке в очередь B не
стартует, пока A не done; после A→done воркер запускает B. Telegram/Plane
показывают Blocked у B до разблокировки (AC-B1/B2/B4).
module: tests/test_orch026_deps_integration.py
expected: PASS
# ---------------- Общие / миграция / регресс ----------------
- id: TC-G01
type: unit
description: >
Аддитивная миграция БД (если выбран вариант с таблицей/колонкой): идемпотентна,
безопасна на существующей БД с данными, не меняет существующие колонки (AC-G4).
module: tests/test_orch026_migration.py
expected: PASS
- id: TC-G02
type: unit
description: >
Наблюдаемость GET /queue: новый блок (заблокированные задачи / держатель lease /
defer-счётчики / циклы) присутствует и read-only; не источник истины.
module: tests/test_orch026_queue_observability.py
expected: PASS
- id: TC-G03
type: integration
description: >
Регресс: полный pytest tests/ -q остаётся зелёным — merge-gate (ORCH-043),
merge-verify (ORCH-073), reconciler (ORCH-053/068), reaper (ORCH-065) не
деградировали (AC-G5).
module: tests/
expected: PASS

View File

@@ -0,0 +1,226 @@
# ADR-001: Сериализация merge/деплоя внутри репо (A) + декларативные зависимости задач (B)
**Work Item:** ORCH-026 · **Repo:** orchestrator (self-hosting) · **Стадия:** architecture
**Статус:** Accepted
**Связи:** дополняет ORCH-043 (merge-gate), ORCH-065 (merge-lease + reclaim), ORCH-073/071
(merge-verify, SHA-in-main), ORCH-1 (очередь). Глобальный ADR — `adr/adr-0015`.
---
## Контекст
ORCH-026 закрывает **первопричину** эрозии `main` 08.06 (некоординированный параллелизм
задач одного репо: ветки от устаревшего `main`, фантом-merge затирает соседа) и попутно вводит
исходный скоуп — декларативные зависимости задач (B ждёт A). Требования — `01-brd.md`,
`02-trz.md`; PASS/FAIL — `03-acceptance-criteria.md`.
Ключевое наблюдение архитектора: **бо́льшая часть инфраструктуры для Уровня A уже существует** и
её НЕ нужно переписывать:
- **merge-lease** (ORCH-065, `src/merge_gate.py`): per-repo файловый лиз
`.merge-lease-<repo>.json`, неблокирующий acquire, holder-aware release, проактивный реклейм
мёртвого/устаревшего держателя (`reclaim_stale_lease`, `pid_alive`). Restart-safe, per-repo.
- **merge-gate** (ORCH-043, `check_branch_mergeable`): на ребре `deploy-staging → deploy`
захватывает лиз, при необходимости ребейзит, держит лиз до фактического merge.
- **defer-механизм** (`_handle_merge_gate_defer`): `merge-lock busy` → повторная постановка
deployer'а через `available_at`, bounded `merge_defer_max_attempts` → эскалация (Blocked+alert).
- **окно лиза** уже простирается от `deploy-staging → deploy` до release на одном из событий:
PR-merged webhook (`gitea.py`), `deploy→done` (`stage_engine.py`), откат, проактивный реклейм.
Для self-hosting `done` достигается ТОЛЬКО после `verify_merged_to_main` (SHA-in-main, ORCH-073).
Таким образом окно сериализации A-1 («merge → main-updated») **структурно уже реализовано**:
пока A не подтверждена в `main` (для self — SHA-in-main → `done`), лиз держится, и B того же
репо на своём merge-gate получает `merge-lock busy` → defer. Открытый вопрос BRD §4.3 (граница
окна для self) решается так: **окно = от acquire до release; release-события не меняем**. Для
non-self репо граница — PR-merged webhook; для self — `deploy→done` (= SHA-in-main подтверждён).
Что реально **отсутствует** для Уровня A:
- **A-2: безусловный proactive pre-merge rebase.** Сейчас `check_branch_mergeable` ребейзит
ТОЛЬКО если `branch_is_behind_main` (⇔ `origin/main` не предок HEAD). AC-A2 требует, чтобы
rebase вызывался **всегда** перед merge — детерминированный структурный анти-фантом на уровне
планировщика, не зависящий от точности ancestor-проверки.
Для Уровня B инфраструктуры нет вовсе: очередь `jobs` (ORCH-1) плоская (FIFO по `id` +
`available_at` + `max_concurrency`), выразить «B ждёт A» нельзя.
---
## Решение
### Уровень A — сериализация merge/деплоя (минимально-инвазивно, переиспользуя ORCH-043/065)
**A-1/A-3/A-4 (окно сериализации) — без изменений механизма.** Окно сериализации обеспечивается
существующим merge-lease: захват в `check_branch_mergeable`, удержание до release. Подтверждаем и
фиксируем в доке, что release-события (`PR-merged` / `deploy→done` / откат / `reclaim_stale_lease`)
формируют окно «merge → main-updated». Кросс-репо параллелизм сохранён автоматически (лиз —
per-repo файл). Restart-safe и анти-залипание — за счёт ORCH-065 reclaim. **Кода-изменений нет.**
**A-2 (безусловный pre-merge rebase) — новое поведение, флаг `premerge_rebase_always`.**
- В `check_branch_mergeable` (`src/qg/checks.py`), ПОД захваченным merge-lease: когда
`settings.premerge_rebase_always` истинно (и merge-gate применим к репо), **пропустить
short-circuit `branch_is_behind_main`** и **всегда** вызвать `merge_gate.auto_rebase_onto_main`.
- `auto_rebase_onto_main` уже идемпотентен и дёшев на актуальной ветке: `git rebase origin/main`
на не-отстающей ветке — no-op (rc 0, HEAD не меняется), последующий `push --force-with-lease`
→ «Everything up-to-date» (тот же SHA, **CI не перезапускается, лишних коммитов нет**). На
отстающей ветке — реальный догон. Текстовый конфликт → существующий контракт: `rebase --abort`
→ откат на `development` (как ORCH-043). **Инвариант: никаких push/force-push в `main`**
единственная force-операция остаётся `--force-with-lease` на ветку задачи.
- Когда флаг выключен → прежнее поведение (ребейз только при `branch_is_behind_main`),
обратная совместимость 1:1 (AC-A7/AC-G2).
- **Скоуп — общий с merge-gate:** реально только для `merge_gate_repos` (пусто → self-hosting
`orchestrator`). Никакого нового scope-флага.
**A-5/A-6 (safety, anti-livelock) — без изменений.** `STAGE_TRANSITIONS`, `QG_CHECKS`,
`Confirm Deploy` (ORCH-059), exit-коды хука, terminal-sync не трогаются. defer-бюджет —
существующий `merge_defer_max_attempts` → Blocked+alert при исчерпании. Прод-контейнер не
рестартится вне штатного `Confirm Deploy`.
### Уровень B — декларативные зависимости (новая инфраструктура)
**B-источник: гибрид с БД как источником истины для планировщика; флаг `task_deps_source`.**
Планировщик `claim_next_job` — горячий цикл, обслуживающий очередь ВСЕХ проектов из ОДНОГО
инстанса. Он **обязан** быть offline-устойчивым и быстрым: сетевой запрос в Plane на каждый claim
= при недоступности Plane встанет конвейер всех проектов (нарушение self-hosting safety). Поэтому:
- **Авторитетный для планировщика стор — локальная БД**, новая аддитивная таблица
`job_deps(task_id, depends_on_task_id, created_at)` (детали — `08-data-requirements.md`).
Связь хранится по `tasks.id` (стабильный локальный ключ). Зависимости — **только внутри одного
репо** (v1; кросс-репо — non-goal, BRD §5).
- **`task_deps_source = db | plane | hybrid`** (дефолт **`db`**): `db` — связи пишутся напрямую в
`job_deps` (потребитель — декомпозиция эпиков ORCH-025); `plane` — связи читаются из Plane
relations в `handle_work_item_created` и **кэшируются** в `job_deps`; `hybrid` — Plane как
декларация + БД-кэш. Plane-ingestion — тонкий add-on за флагом; планировщик ВСЕГДА читает БД.
**B-2 (гейт планировщика) — SQL `NOT EXISTS`, без занятия слота `max_concurrency`.**
Гейт готовности выражается декларативно в `claim_next_job` (`src/db.py`): задача claimable, если
у неё нет ни одной незавершённой зависимости. Когда `settings.task_deps_enabled` — к существующему
SELECT добавляется условие:
```sql
AND NOT EXISTS (
SELECT 1 FROM job_deps d
JOIN tasks t ON t.id = d.depends_on_task_id
WHERE d.task_id = j.task_id AND t.stage != 'done'
)
```
Это: (1) **не занимает слот** — job просто не выбирается, агент не запускается (AC-B2);
(2) restart-safe (чистая БД); (3) never-raise (это SQL); (4) при пустой `job_deps`
инертно (нулевая регрессия, AC-G2); (5) при выключенном `task_deps_enabled` условие НЕ
добавляется → запрос 1:1 как в ORCH-1. Как только все зависимости достигают `stage='done'`,
задача автоматически становится claimable.
Чистая leaf-логика «готова ли задача» выносится в новый модуль `src/task_deps.py`:
`is_task_ready(task_id) -> (bool, waiting_on: list[str])` (never-raise) — для реконсилятора,
карточки и `/queue` (SQL в `claim_next_job` — горячий путь, дублирует ту же семантику).
**B-3 (детект дедлоков) — DFS, чистая функция.**
`task_deps.detect_cycle(task_id) -> list[int] | None` — обход графа `job_deps` (внутри репо),
детерминированный, юнит-тестируемый, never-raise. Запускается: (1) при вставке связи
(`add_dependency`) — цикл отклоняется/алертится сразу (лучший UX); (2) backstop-проход в тике
`reconciler` (на случай связей, добавленных в обход). Цикл → `set_issue_blocked(work_item_id)` +
Telegram/Plane alert с перечислением цикла. SQL-гейт B-2 сам по себе никогда не выберет задачу в
цикле (её зависимости не достигнут `done`) — детектор делает это **видимым**, а не молчаливым
вечным ожиданием (AC-B3). Поток остальных задач не блокируется.
**B-4 (видимость).**
- Нормальное ожидание (B ждёт A, A в работе — транзиентно и ожидаемо): строка в Telegram-карточке
«⏳ ждёт ORCH-NNN» через `notifications.update_task_tracker`, never-raise/silent. **Plane Blocked
при нормальном ожидании НЕ ставим** — иначе флаппинг Blocked на каждом коротком ожидании.
- Дедлок/цикл (B-3): `set_issue_blocked` (Plane `Blocked`) + alert. Это «и/или» из AC-B4.
- Инвариант «одна карточка на задачу» сохранён (ORCH-042/067).
**B-5 (совместимость reconciler/reaper).**
- `reconciler` F-1 не должен «разблокировать» dep-заблокированную задачу мимо её зависимостей.
В фильтр пригодности reconciler добавляется проверка `task_deps.is_task_ready` (по образцу
`reconcile_skip_blocked_enabled`, ORCH-060): не готова → skip.
- `reaper` сканирует **`running`** jobs; dep-заблокированный job остаётся `queued` (его не
клеймят) → reaper его не трогает по построению. Фиксируем в доке.
**Наблюдаемость (TRZ §4):** блок `task_deps` в снимке `GET /queue` (read-only, по образцу
`reconcile`/`reaper`): кол-во заблокированных задач, держатель merge-lease, defer-счётчики,
обнаруженные циклы. Никогда не источник решений.
### Конфигурация (`src/config.py`)
| Флаг | Дефолт | Назначение |
|------|--------|-----------|
| `premerge_rebase_always` | `True` | Уровень A: безусловный pre-merge rebase под лизом. Скоуп — `merge_gate_repos`. Kill-switch (`False` → ребейз только при behind, как ORCH-043). |
| `task_deps_enabled` | `True` | Уровень B: глобальный kill-switch гейта зависимостей. `False``claim_next_job` 1:1 как ORCH-1. Инертно при пустой `job_deps`. |
| `task_deps_source` | `"db"` | Источник деклараций: `db`\|`plane`\|`hybrid`. Планировщик всегда читает БД-кэш. |
Дефолты следуют конвенции репо (`*_enabled=True` + kill-switch), при этом обе фичи инертны без
данных (нет деклараций / нет применимых репо) → нулевая регрессия для enduro-trails.
---
## Альтернативы (и почему отвергнуты)
1. **Уровень A — отдельный глобальный per-repo merge-lock или FIFO merge-queue.** Дублировал бы
уже существующий merge-lease (ORCH-065), вводил второй механизм сериализации с риском
рассинхрона. Отвергнуто: BRD §4.2 требует минимально-инвазивного решения, переиспользующего
ORCH-065/043. Окно лиза уже даёт сериализацию.
2. **Уровень A — расширять release-точки лиза (держать до отдельного `main-updated`-события).**
Не требуется: для self `done` ⇔ SHA-in-main (ORCH-073), для non-self — PR-merged webhook;
окно уже корректно. Доп. событие усложнило бы reclaim без выигрыша.
3. **Уровень B — Plane relations как источник истины планировщика.** Сетевой запрос в горячем
цикле claim; при недоступности Plane встаёт очередь всех проектов (self-hosting risk).
Отвергнуто; Plane оставлен опциональным источником **декларации** (`task_deps_source=plane`),
но планировщик читает только БД-кэш.
4. **Уровень B — гейт зависимостей в воркере (`_drain_once`) поверх `claim_next_job`.** Пришлось
бы клеймить job, обнаруживать незавершённую зависимость и re-queueить — churn, расход attempts,
гонки. SQL `NOT EXISTS` в самом `claim_next_job` чище: job просто не выбирается, слот свободен.
5. **Уровень B — поле/JSON в `tasks` вместо таблицы.** Таблица `job_deps` нормальна (M:N),
индексируема, проще для DFS и `NOT EXISTS`. Поле в `tasks` потребовало бы парсинг-логики.
---
## Последствия
**Плюсы.**
- Минимально-инвазивно: Уровень A — один флаг + снятие short-circuit; окно сериализации не
переписывается. Переиспользует ORCH-043/065 целиком.
- Уровень B — одно `NOT EXISTS` в `claim_next_job` + аддитивная таблица + leaf-модуль
`task_deps.py`; `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты (паттерн врезки ORCH-071/058).
- Обе фичи инертны без данных → нулевая регрессия для enduro-trails (AC-A7/AC-G2).
- restart-safe (БД + файловый лиз), never-raise, kill-switch на каждую фичу.
**Минусы / ограничения.**
- `premerge_rebase_always=True` добавляет (дешёвый, no-op на актуальной ветке) `rebase`+`push`
на каждый self-merge. Цена — лишний git-вызов; компенсируется детерминизмом анти-фантома.
- Уровень B v1 — только intra-repo зависимости; кросс-репо — follow-up (non-goal).
- Гейт B-2 в `claim_next_job` слегка усложняет горячий SQL (один `NOT EXISTS`); защищён
kill-switch и инертностью при пустой таблице.
- `task_deps.py` цикл-детектор — новая поверхность; покрывается юнит-тестами (`04-test-plan.yaml`).
**Инварианты (не нарушать).**
1. `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`/`check_staging_status`,
`Confirm Deploy` (ORCH-059), БАГ-8, terminal-sync — без изменений.
2. Никаких push/force-push в `main`; force только `--force-with-lease` на ветку задачи.
3. Сериализация — строго per-repo; кросс-репо параллелизм сохранён.
4. never-raise во всех новых функциях; restart-safe состояние; миграция БД только аддитивная.
5. ORCH-026 **дополняет** рубежи ORCH-073, не заменяет.
6. Прод-контейнер orchestrator не рестартится вне штатного `Confirm Deploy`.
**Места реализации (для developer).**
- `src/qg/checks.py::check_branch_mergeable` — ветка `premerge_rebase_always`.
- `src/db.py::claim_next_job` — условный `NOT EXISTS`-гейт; новые helpers `add_dependency`,
`get_dependencies`, `job_deps` миграция в `init_db` (`CREATE TABLE IF NOT EXISTS`).
- `src/task_deps.py` (новый, leaf) — `is_task_ready`, `detect_cycle`, snapshot для `/queue`.
- `src/webhooks/plane.py::handle_work_item_created` — ingestion Plane relations (за `task_deps_source`).
- `src/reconciler.py` — skip dep-заблокированных + backstop цикл-детект.
- `src/notifications.py` — строка ожидания в карточке.
- `src/config.py``premerge_rebase_always`, `task_deps_enabled`, `task_deps_source`.
- Документация: `docs/architecture/README.md`, `CLAUDE.md` (если меняется поведение очереди),
`CHANGELOG.md`, глобальный `adr/adr-0015`.

View File

@@ -0,0 +1,65 @@
# 08 — Требования к схеме БД — ORCH-026
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** architecture
**Связь:** ADR `06-adr/ADR-001-merge-serialization-and-task-deps.md` (Уровень B).
> Уровень A (сериализация merge/деплоя) — **БЕЗ изменения схемы БД** (merge-lease файловый,
> `.merge-lease-<repo>.json`, ORCH-065). Изменения схемы касаются ТОЛЬКО Уровня B.
---
## Новая таблица `job_deps` (аддитивная)
Хранит декларативные зависимости «задача `task_id` ждёт задачу `depends_on_task_id`».
```sql
CREATE TABLE IF NOT EXISTS job_deps (
task_id INTEGER NOT NULL, -- tasks.id зависимой задачи (B)
depends_on_task_id INTEGER NOT NULL, -- tasks.id задачи-предшественника (A)
created_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (task_id, depends_on_task_id)
);
CREATE INDEX IF NOT EXISTS idx_job_deps_task ON job_deps(task_id);
CREATE INDEX IF NOT EXISTS idx_job_deps_depends ON job_deps(depends_on_task_id);
```
### Поля
| Поле | Тип | Назначение |
|------|-----|-----------|
| `task_id` | INTEGER | `tasks.id` зависимой задачи (B). Не запускается, пока зависимости не `done`. |
| `depends_on_task_id` | INTEGER | `tasks.id` предшественника (A). Терминальность — `tasks.stage = 'done'`. |
| `created_at` | TEXT | Время декларации (диагностика). |
### Ключ и индексы
- **PK `(task_id, depends_on_task_id)`** — идемпотентность вставки (повторная декларация связи —
no-op через `INSERT OR IGNORE`), запрет дублей.
- `idx_job_deps_task` — гейт планировщика (`NOT EXISTS ... WHERE d.task_id = j.task_id`).
- `idx_job_deps_depends` — обратные рёбра для DFS цикл-детектора.
### Семантика готовности (источник истины планировщика)
Задача `task_id` **готова к запуску** ⇔ нет ни одной строки `job_deps` для неё, чей
`depends_on_task_id` указывает на задачу с `tasks.stage != 'done'`. Терминал — только `done`
(совпадает с тем, как `get_active_tasks_for_reconcile` трактует терминальность).
### Связь по `task_id`, а не `work_item_id`
`tasks.id` — стабильный локальный автоинкремент-ключ; `work_item_id`/`plane_id` могут
ресолвиться/коллизиться (см. `ensure_unique_work_item_id`). FK логический (без `REFERENCES`,
как у `jobs.task_id`) — не блокирует аддитивную миграцию и удаление строк tasks (которого в
конвейере нет). Зависимости — **только intra-repo** (v1); кросс-репо рёбра не создаются.
---
## Миграция (AC-G4)
- Выполняется в `src/db.py::init_db` рядом с прочими: **только** `CREATE TABLE IF NOT EXISTS` +
`CREATE INDEX IF NOT EXISTS`. **Идемпотентно**, restart-safe, безопасно на живой общей прод-БД.
- **Существующие колонки/таблицы (`jobs`, `tasks`, `agent_runs`, `events`) НЕ изменяются** →
данные enduro-trails не затронуты.
- Откат фичи — флагом `task_deps_enabled=False` (таблица остаётся, гейт не применяется); сама
таблица деструктивно не удаляется.
## Что НЕ меняется
- Схема `jobs` (включая `available_at`, `pid`, `attempts`/`transient_attempts`) — без изменений;
defer Уровня A/B переиспользует существующий `available_at`-механизм.
- Схема `tasks` — без изменений (видимость через существующие `tracker_message_id` и Plane Blocked).
- merge-lease — файловый, вне БД.

View File

@@ -0,0 +1,17 @@
# 10 — Технические риски — ORCH-026
**Work Item:** ORCH-026 · **Repo:** orchestrator · **Стадия:** architecture
**Связь:** ADR `06-adr/ADR-001-merge-serialization-and-task-deps.md`.
| # | Риск | Уровень | Митигация |
|---|------|---------|-----------|
| R-1 | **Гейт `NOT EXISTS` в `claim_next_job` (горячий путь всех проектов) содержит баг → встаёт очередь ВСЕХ проектов** (self-hosting групповой риск). | Высокий | Условие добавляется ТОЛЬКО при `task_deps_enabled`; инертно при пустой `job_deps` (нулевая регрессия); kill-switch `task_deps_enabled=False` мгновенно возвращает поведение ORCH-1; интеграционный тест «пустые deps ⇒ FIFO 1:1» (AC-G2). |
| R-2 | **Безусловный `premerge_rebase_always` делает лишний `push --force-with-lease` → ложный перезапуск CI / новые коммиты.** | Низкий | На актуальной ветке `rebase origin/main` — no-op (HEAD не меняется), push → «Everything up-to-date» (тот же SHA, CI не триггерится). Подтвердить тестом, что SHA не меняется на уже-актуальной ветке. |
| R-3 | **Дедлок по циклической зависимости → задача молча ждёт вечно.** | Средний | DFS-детектор `detect_cycle` при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert с перечислением цикла (AC-B3); SQL-гейт не выбирает задачу в цикле, детектор делает это видимым. |
| R-4 | **Livelock: B бесконечно deferится на `merge-lock busy`.** | Низкий | Существующий bounded-бюджет `merge_defer_max_attempts` → Blocked+alert (ORCH-043, без изменений). |
| R-5 | **Залипший merge-lease после смерти держателя → конвейер репо встаёт навсегда.** | Средний | Переиспользуется ORCH-065: `reclaim_stale_lease` (мёртвый `pid` / TTL `merge_lock_timeout_s`) + holder-aware release. Restart-safe (AC-A4). |
| R-6 | **Plane relations недоступны/неверно смаплены при `task_deps_source=plane`.** | Средний | Планировщик читает ТОЛЬКО БД-кэш `job_deps`; Plane-ingestion — best-effort, never-raise; дефолт `task_deps_source=db` не зависит от Plane. |
| R-7 | **reconciler «разблокирует» dep-заблокированную задачу мимо её зависимостей.** | Средний | В фильтр reconciler добавляется `is_task_ready` (паттерн ORCH-060 skip-Blocked); reaper трогает только `running` — dep-блок остаётся `queued` (AC-B5). |
| R-8 | **Миграция БД повреждает общую прод-БД (данные enduro-trails).** | Низкий | Только аддитивно: `CREATE TABLE/INDEX IF NOT EXISTS`; существующие колонки не меняются; идемпотентно (AC-G4). |
| R-9 | **Self-hosting: изменения требуют рестарта прод-контейнера вне `Confirm Deploy`.** | Высокий (если нарушено) | Все изменения — обычный код, проходят `deploy-staging` (8501) → `Confirm Deploy` (ORCH-059). `STAGE_TRANSITIONS`/`QG_CHECKS` не трогаются; никакого внеочередного рестарта (AC-A5). |
| R-10 | **Конфликт точек интеграции A (merge-gate) и B (постановка в очередь).** | Низкий | Разные точки конвейера: B гейтит claim job (вход), A гейтит merge на ребре `deploy-staging→deploy`. Независимы; покрыть интеграционным тестом совместной работы (BRD §4.4). |

View File

@@ -0,0 +1,47 @@
---
type: review
work_item_id: ORCH-026
verdict: APPROVED
version: 1
---
# Review ORCH-026
## Summary
ORCH-026 реализует два уровня по ADR-001: **Уровень A** — сериализация merge/deploy внутри одного репо (переиспользует merge-lease ORCH-043/065 + единственная новая логика — безусловный pre-merge rebase под флагом `premerge_rebase_always`) и **Уровень B** — декларативные зависимости задач (аддитивная таблица `job_deps`, гейт `NOT EXISTS` в `claim_next_job`, leaf-модуль `src/task_deps.py`). Реализация минимально-инвазивна, строго соответствует ТЗ и ADR, обе фичи условны (kill-switch) и инертны без данных. Все 16 критериев приёмки выполнены. Полный прогон `pytest tests/ -q`**991 passed**, из них 50 новых ORCH-026-тестов зелёные. Документация обновлена в том же PR. **APPROVED.**
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice to have
- [ ] PR-ветка несёт коммиты ORCH-073 (`main` ещё не получил merge #77, merge-base = `77abfb3`). Это ожидаемо по топологии (ORCH-026 (B) построен поверх уже отревьюенного предшественника ORCH-073 (A): у ORCH-073 есть собственные `12-review.md`/`13-test-report.md`/`14-deploy-log.md`) и фактически демонстрирует саму фичу A (rebase B на код A). Не блокирует; при merge в `main` приедут оба набора изменений — это корректно.
## Соответствие ТЗ и ADR
- **Уровень A (AC-A1…A7):** окно сериализации обеспечено существующим merge-lease без нового механизма (ADR §A-1/A-3/A-4). A-2 — `check_branch_mergeable` (`src/qg/checks.py`) под лизом при `premerge_rebase_always=True` всегда вызывает `auto_rebase_onto_main`, снимая short-circuit `branch_is_behind_main`; kill-switch off → поведение ORCH-043 1:1. `STAGE_TRANSITIONS`/`QG_CHECKS`/`Confirm Deploy` не тронуты — соответствует инвариантам §9. Никаких push/force в `main` (только `--force-with-lease` ветки).
- **Уровень B (AC-B1…B5):** гейт `NOT EXISTS (job_deps JOIN tasks WHERE stage!='done')` в `claim_next_job` (`src/db.py`) — job не выбирается, слот `max_concurrency` не занимается; при выключенном флаге / пустой таблице clause не добавляется (нулевая регрессия). `task_deps.py` — чистый leaf: `is_task_ready` (fail-open), итеративный WHITE/GREY/BLACK DFS-детектор циклов (защита от recursion-limit на проде), `handle_cycle` (Blocked+alert), `declare_dependency`, `ingest_plane_relations` (только `plane|hybrid`, дефолт `db` не ходит в сеть на горячем пути). reconciler F-1 получил Guard 3 (skip dep-заблокированных + backstop детект цикла); reaper не тронут (сканирует `running`).
- **Общие (AC-G1…G5):** контракт never-raise выдержан во всех новых функциях (try/except, консервативная деградация). Миграция строго аддитивна — `CREATE TABLE/INDEX IF NOT EXISTS`, без `REFERENCES`, схема `tasks`/`jobs` не изменена (AC-G4 OK на живой общей БД). Наблюдаемость — read-only блок `task_deps` в `GET /queue`. Реализация в точности по местам, указанным в ADR §«Места реализации».
## Качество кода
- Docstrings на всех публичных функциях, явно документирован контракт fail-open/fail-closed.
- SQL-гейт безопасен: `dep_gate` — константная строка (нет инъекции), таблица `job_deps` гарантированно создана в `init_db`.
- Переменные `plane_id`/`plane_project_id`/`task_id` в `start_pipeline` — в области видимости (проверено).
- Тесты содержательные: миграция, conditionality (kill-switch), циклы, видимость, observability, интеграция сериализации и зависимостей.
## Документация — обновлена (golden source)
Проверено: код в `src/` изменён → документация обновлена В ТОМ ЖЕ PR (разнесена по pipeline-коммитам ветки, что нормально):
- `docs/architecture/README.md` — разделы про очередь (`claim_next_job`-гейт), pre-merge rebase, «Зависимости задач: B ждёт A», `job_deps`, наблюдаемость (architect-коммит `f8ec1c2`). ✓
- `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md` + глобальный `docs/architecture/adr/adr-0015-task-deps-and-merge-serialization.md`. ✓
- `CLAUDE.md` — паспорт (очередь/сериализация). ✓
- `CHANGELOG.md` — запись `## [Unreleased]`. ✓
- `.env.example``ORCH_PREMERGE_REBASE_ALWAYS`/`ORCH_TASK_DEPS_ENABLED`/`ORCH_TASK_DEPS_SOURCE`. ✓
- `08-data-requirements.md` — таблица `job_deps`. ✓
Документация = golden source: требование выполнено.

View File

@@ -0,0 +1,75 @@
---
type: test-report
work_item_id: ORCH-026
result: PASS
---
# Test Report — ORCH-026
Задача: «Управление зависимостями задач (B ждёт A) в очереди» + сериализация merge/деплоя
одного репо. Ветка `feature/ORCH-026-b-a`. Review-вердикт: **APPROVED** (`12-review.md`).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: `feature/ORCH-026-b-a` (HEAD `aaa4829`)
- Прод-оркестратор (8500): `/health``{"status":"ok"}` (не перезапускался, self-hosting инвариант соблюдён)
- Дата: 2026-06-08
## Результаты по тест-плану (04-test-plan.yaml)
### Уровень A — сериализация merge/деплоя
| TC ID | Описание | Тест-функция | Результат |
|-------|----------|--------------|-----------|
| TC-A01 | Proactive pre-merge rebase (всегда, даже когда не behind) | `test_orch026_premerge_rebase::test_always_rebases_even_when_not_behind` | PASS |
| TC-A02 | Расширенное окно merge-lease, defer не откат; holder-aware release | `test_orch026_merge_serialize::test_second_task_same_repo_defers_not_rollback`, `test_holder_aware_release_keeps_foreign_lease` | PASS |
| TC-A03 | Сериализация строго per-repo (orchestrator ≠ enduro-trails) | `test_orch026_merge_serialize::test_serialization_is_strictly_per_repo` | PASS |
| TC-A04 | Restart-safe + реклейм мёртвого держателя lease | `test_orch026_merge_serialize::test_dead_holder_lease_is_reclaimed`, `test_stale_lease_age_reclaimed_on_acquire` | PASS |
| TC-A05 | Anti-livelock defer: bounded бюджет, эскалация | `test_orch026_merge_serialize::test_defer_budget_is_bounded` | PASS |
| TC-A06 | Условность/kill-switch: off + out-of-scope = no-op | `test_orch026_conditionality::test_out_of_scope_repo_is_noop_even_with_flag_on`, `test_premerge_rebase::test_flag_off_short_circuits_like_orch043` | PASS |
| TC-A07 | Self-hosting safety: только `--force-with-lease` на ветку, STAGE_TRANSITIONS не тронуты | `test_orch026_conditionality::test_premerge_only_force_with_lease_on_branch`, `test_stage_transitions_unchanged` | PASS |
| TC-A08 | Сквозной сценарий сериализации merge-окна | `test_orch026_serialize_integration::test_serialized_merge_window` | PASS |
### Уровень B — декларативные зависимости
| TC ID | Описание | Тест-функция | Результат |
|-------|----------|--------------|-----------|
| TC-B01 | Декларация/резолв blocked-by; never-raise при недоступности | `test_orch026_task_deps::test_add_dependency_declares_and_resolves`, `test_add_dependency_never_raises_on_bad_input` | PASS |
| TC-B02 | Гейт готовности: незавершённый depends-on → не ready; все done → ready | `test_orch026_task_deps::test_is_task_ready_blocked_then_ready`, `test_is_task_ready_no_deps_is_ready` | PASS |
| TC-B03 | Детект циклов A→B→A и длиннее; ацикличный → нет | `test_orch026_dep_cycles::test_detect_two_node_cycle`, `test_detect_longer_cycle`, `test_acyclic_graph_has_no_cycle`, `test_detect_cycle_never_raises_on_garbage` | PASS |
| TC-B04 | Цикл → Blocked + alert без падения воркера | `test_orch026_dep_cycles::test_handle_cycle_blocks_and_alerts`, `test_handle_cycle_never_raises_when_notify_fails` | PASS |
| TC-B05 | claim_next_job не клеймит заблокированную (слот свободен), разблокируется при done | `test_orch026_task_deps::test_claim_skips_dep_blocked_job`, `test_claim_prefers_unblocked_job_over_blocked` | PASS |
| TC-B06 | Видимость: строка ожидания в карточке; never-raise рендер | `test_orch026_dep_visibility::test_blocked_task_shows_waiting_line`, `test_render_never_raises_on_dep_error` | PASS |
| TC-B07 | reconciler F-1 не разблокирует dep-заблокированную | `test_orch026_task_deps::test_reconciler_skip_helper_honours_block` | PASS |
| TC-B08 | Сквозной: B стартует только после A→done; multiple predecessors | `test_orch026_deps_integration::test_b_waits_for_a_then_runs`, `test_multiple_predecessors_all_must_be_done`, `test_ingest_plane_relations_writes_db` | PASS |
### Общие / миграция / регресс
| TC ID | Описание | Тест-функция | Результат |
|-------|----------|--------------|-----------|
| TC-G01 | Аддитивная миграция job_deps: идемпотентна, данные сохранены | `test_orch026_migration::test_job_deps_table_created`, `test_job_deps_indices_created`, `test_migration_idempotent_and_preserves_data` | PASS |
| TC-G02 | Наблюдаемость GET /queue: read-only блок task_deps | `test_orch026_queue_observability::test_queue_endpoint_includes_task_deps`, `test_snapshot_*` | PASS |
| TC-G03 | Регресс: полный pytest зелёный | `tests/` (991 passed) | PASS |
## Smoke test API (прод 8500)
- `GET /health``{"status":"ok","service":"orchestrator"}` — OK
- `GET /status` → активные задачи отдаются, ORCH-026 (id 58) в стадии `testing` — OK
- `GET /queue` → counts/resilience/reconcile/reaper/merge_verify читаются; брейкер `closed`, preflight OK — OK
- Примечание: блок `task_deps` в `/queue` прода 8500 ОТСУТСТВУЕТ — ожидаемо: прод-контейнер несёт текущую задеплоенную версию, ORCH-026 ещё не выкатан (self-hosting, деплой на поздних стадиях). Фича наблюдаемости верифицирована in-branch тестом `test_queue_endpoint_includes_task_deps` (PASS) через TestClient на коде ветки.
## Вывод pytest
```
tests/test_orch026_*.py — 50 passed, 1 warning in 1.56s
tests/ — 991 passed, 1 warning in 26.52s
```
(единственный warning — PydanticDeprecatedSince20 в `src/config.py`, предсуществующий, не относится к ORCH-026)
## Покрытие критериев приёмки (03-acceptance-criteria.md)
Все 16 критериев (AC-A1…A7, AC-B1…B5, AC-G1…G5) покрыты прохождением соответствующих TC и
подтверждены review-вердиктом APPROVED. Регрессии merge-gate (ORCH-043), merge-verify
(ORCH-073), reconciler (ORCH-053/068), reaper (ORCH-065) не обнаружено.
## Итог
**PASS** — 50/50 новых ORCH-026-тестов зелёные, полный регресс 991 passed, smoke API OK,
прод-контейнер не затронут. Задача готова к переходу на `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-026
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,34 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T16:14:11+00:00
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. Exit code 0 → advance.
Canonical run (ORCH-048, ADR-001) inside the live staging container:
```
docker exec orchestrator-staging \
python3 /repos/orchestrator/scripts/staging_check.py \
--base-url http://localhost:8501 --mode stub
```
## Result: 8/10 checks PASS
- **Block A (SMOKE):** A1 /health, A2 /queue, A3 ORCH_STAGING=true — all PASS.
- **Block B (ACCESS):** B4 Plane sandbox (R), B5 Gitea orchestrator-sandbox (R+push), B6 registry isolation (sandbox present, prod ET/ORCH absent) — all PASS.
- **Block C (E2E, stub):** C7 create issue, C8 trigger pipeline — PASS.
REAL failed: **none** — all pipeline checks green.
## INFRA-WAIVED (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
```
C9a/C9b are the two known sandbox-infra-only checks (depend on SANDBOX bot accounts being members of the sandbox Plane project, not on the pipeline). They are tolerated because every REAL check is green; the script printed `INFRA-WAIVED:` and exited 0 (fail-closed semantics preserved: any REAL failure would still yield exit 1).

View File

@@ -0,0 +1,7 @@
# Business Request: Approve деплоя через статус Confirm Deploy (вместо перегруженного Approved)
Work Item ID: ORCH-059
## Description
TBD

View File

@@ -0,0 +1,115 @@
# 01 — BRD: Approve прод-деплоя через выделенный статус «Confirm Deploy»
Work Item: **ORCH-059**
Repo: `orchestrator`
Stage: analysis
Тип: enhancement / risk-reduction (self-hosting)
## 1. Контекст и проблема
В ORCH-036 («исполняемый самодеплой стадии `deploy`») прод-деплой self-hosting
инстанса (контейнер `orchestrator`, порт 8500) запускается **Фазой B**: человек
переводит issue в Plane-статус **`Approved`**, webhook
`work_item.updated``handle_issue_updated``handle_verdict(approved=True)`
`_try_advance_stage``advance_stage(finished_agent=None)`, и в
`stage_engine.advance_stage` срабатывает блок
`current_stage == "deploy" and finished_agent is None`
`_handle_self_deploy_phase_b` → detached host-деплой прода.
**Перегрузка статуса.** Тот же самый Plane-статус `Approved` (UUID
`a519a341-…`) используется как **человеческий гейт одобрения BRD** на ранней
стадии `analysis` (`check_analysis_approved`: analysis → architecture) и в общем
verdict-роутинге `handle_verdict`. Один и тот же визуальный «Approved» на доске
означает две принципиально разные вещи:
- на `analysis` — «BRD/ТЗ/AC приняты, продолжай конвейер» (дёшево, обратимо);
- на `deploy` — «**ВЫКАТИ В ПРОД** инструмент, который прямо сейчас обслуживает
все проекты из одного инстанса с общей БД» (дорого, групповой риск, см.
раздел Self-hosting в `CLAUDE.md`).
### Последствия (Pain)
- **Двусмысленность семантики.** Один статус — два смысла; оператор не видит из
названия, что клик на `deploy` запускает реальный прод-рестарт.
- **Риск случайного клика.** Привычный жест «Approved» (которым оператор
штатно одобряет BRD десятки раз) на стадии `deploy` молча триггерит
прод-деплой. Цена ошибки — незапланированный рестарт прод-инстанса,
встающий конвейер всех проектов.
- **Несоответствие ожиданиям ORCH-036.** В scope ORCH-36 заявлялась Telegram
inline-кнопка подтверждения; в коде её **нет** — developer реализовал approve
исключительно через Plane-статус. Отдельного «осознанного» жеста подтверждения
деплоя в системе сейчас не существует.
## 2. Решение Owner
Ввести **отдельный Plane-статус `Confirm Deploy`** в проекте ORCH, который
триггерит **ТОЛЬКО** Фазу B self-deploy на стадии `deploy`. Статус `Approved`
перестаёт запускать прод-деплой и сохраняет единственный смысл — человеческое
одобрение на гейтах конвейера (прежде всего BRD на `analysis`).
Минимальная правка: `handle_verdict` в `src/webhooks/plane.py` + регистрация
нового состояния в проекте ORCH (Plane + резолвер состояний).
## 3. Бизнес-цели
- **BG-1.** Убрать двусмысленность: жест «запустить прод-деплой» отделён от жеста
«одобрить артефакт».
- **BG-2.** Снизить риск случайного прод-деплоя: запуск прода требует явного,
редко используемого статуса `Confirm Deploy`, а не привычного `Approved`.
- **BG-3.** Не сломать работающий self-hosting конвейер при доработке самого
инструмента (нулевая регрессия `analysis`-гейта и не-self репозиториев).
## 4. Объём (Scope)
### В объёме
- Новый логический статус `confirm_deploy` («Confirm Deploy») в резолвере
состояний Plane (`src/plane_sync.py`).
- Маршрутизация нового статуса в `src/webhooks/plane.py`
(`handle_issue_updated` / `handle_verdict`) на путь Фазы B прод-деплоя.
- Прекращение триггера Фазы B по статусу `Approved` на стадии `deploy`.
- Обновление текста CTA Фазы A (Plane-комментарий + Telegram в
`stage_engine._handle_self_deploy_phase_a`): инструктировать оператора
переводить задачу в `Confirm Deploy`, а не в `Approved`.
- Конфигурация Plane: создание статуса «Confirm Deploy» в проекте ORCH
(предусловие эксплуатации — фиксируется в TRZ/AC как требование среды).
- Обновление документации (`CLAUDE.md`, `docs/architecture/README.md` секция
ORCH-036, `CHANGELOG.md`) и ADR per-work-item.
### Вне объёма
- Telegram inline-кнопки подтверждения деплоя (отдельная задача; здесь не
реализуем — управление по-прежнему статусом Plane).
- Полностью автоматический approve деплоя (ORCH-54).
- Изменение Фаз A/C, exit-кодов хука, merge-gate, `check_deploy_status`,
схемы БД, реестров `STAGE_TRANSITIONS` / `QG_CHECKS`.
- Поведение прод-деплоя для не-self репозиториев (остаётся прежним).
- Post-deploy наблюдение (ORCH-021) — не затрагивается.
## 5. Заинтересованные стороны
- **Owner/оператор** — переводит задачи по статусам; главный выгодоприобретатель
снижения риска.
- **Self-hosting конвейер** — все проекты на общем инстансе; косвенно зависят от
безопасности прод-деплоя орка.
## 6. Допущения
- A-1. Plane позволяет добавить кастомный статус «Confirm Deploy» в проект ORCH;
его UUID резолвится через `get_project_states` (API `/states/`).
- A-2. Статус `Confirm Deploy` нужен только проекту ORCH (self-hosting). Прочие
проекты прод-деплой через Plane-approve не используют
(`self_deploy_applies` → только `orchestrator`).
- A-3. Оператор переводит задачу в `Confirm Deploy` только когда она реально
находится на стадии `deploy` (approval-pending после Фазы A).
## 7. Риски (детально — 10-tech-risks.md, ведёт архитектор)
- R-1. Новый логический ключ `confirm_deploy` отсутствует в fallback
`_DEFAULT_STATES` и в проектах без этого статуса → обращение к ключу должно
быть безопасным (fail-closed: нет статуса → нет деплоя, не падение).
- R-2. Регрессия: `Approved` на `deploy` после правки не должен НИ
запускать деплой, НИ вызывать ложный откат/advance.
- R-3. Самоправка прода: правка не должна потребовать ручного рестарта прод-
контейнера вне штатной стадии deploy-staging → deploy.
## 8. Definition of Done (бизнес-уровень)
- Перевод задачи стадии `deploy` в `Confirm Deploy` запускает прод-деплой
(Фаза B) ровно так же, как раньше делал `Approved`.
- Перевод задачи стадии `deploy` в `Approved` прод-деплой НЕ запускает.
- `Approved` на `analysis` (и прочих человеческих гейтах) работает без изменений.
- CTA Фазы A просит `Confirm Deploy`.
- Документация и ADR обновлены в том же PR.

View File

@@ -0,0 +1,103 @@
# 02 — ТЗ: выделенный статус «Confirm Deploy» как триггер прод-деплоя
Work Item: **ORCH-059** · Repo: `orchestrator` · Stage: analysis
> ТЗ описывает **что** должно измениться и **поведенческий контракт**. Конкретный
> дизайн (сигнатуры, способ проброса признака «confirm-deploy» из webhook в
> `stage_engine`, sentinel-обработка) — за архитектором (ADR per-work-item).
> Точки касания ниже заданы бизнес-запросом Owner и текущей реализацией ORCH-036.
## 1. Задействованные модули `src/`
| Модуль | Роль в задаче |
|--------|---------------|
| `src/plane_sync.py` | Резолвер состояний Plane. Добавить логический ключ `confirm_deploy` ↔ имя статуса «Confirm Deploy»; обеспечить безопасный доступ при отсутствии статуса (fallback/неполный конфиг). |
| `src/webhooks/plane.py` | `handle_issue_updated` — маршрутизация нового статуса; `handle_verdict` — отделить «подтверждение деплоя» от обычного approve; снять триггер Фазы B со статуса `Approved` на `deploy`. |
| `src/stage_engine.py` | Блок Фазы B (`current_stage == "deploy" and finished_agent is None`) должен срабатывать ТОЛЬКО по сигналу confirm-deploy, не по обычному Approved. Обновить CTA-текст Фазы A (`_handle_self_deploy_phase_a`). |
| `src/config.py` | (опционально, на усмотрение архитектора) флаг/имя статуса, если потребуется конфигурируемость. По умолчанию — не требуется. |
## 2. Поведенческий контракт (требования)
### TRZ-1. Регистрация статуса «Confirm Deploy»
Резолвер состояний (`get_project_states`) обязан возвращать UUID статуса
«Confirm Deploy» под логическим ключом `confirm_deploy` для проекта ORCH.
Маппинг имени `"Confirm Deploy" → "confirm_deploy"` добавляется в
`_PLANE_NAME_TO_KEY`. Для проектов/сред, где статус отсутствует (enduro,
fallback `_DEFAULT_STATES`, недоступный API), ключ может отсутствовать —
обращение к нему должно быть **fail-closed**: «нет статуса → ветка confirm-deploy
не активируется», без `KeyError`/исключения.
### TRZ-2. Триггер прод-деплоя по «Confirm Deploy»
Когда задача находится на стадии `deploy` и issue переводится в статус
`Confirm Deploy`, система обязана инициировать **Фазу B** прод-деплоя
(эквивалент текущего `_handle_self_deploy_phase_b`: idempotency-guard `initiated`,
`self_deploy.initiate_deploy`, постановка `deploy-finalizer`, комментарии/Telegram).
Поведение, идемпотентность и Фаза C — **без изменений** относительно ORCH-036;
меняется только **что именно является триггером**.
### TRZ-3. `Approved` больше не запускает прод-деплой
Перевод задачи стадии `deploy` в статус `Approved` **не должен** инициировать
Фазу B. Он не должен также вызывать ложный откат (БАГ-8) или ложный advance
по `check_deploy_status` (вердикта ещё нет). Допустимое поведение — **no-op с
логированием** (issue остаётся на `deploy`/approval-pending). Конкретный способ
(игнор на уровне webhook-роутинга или на уровне `stage_engine`) — за архитектором.
### TRZ-4. Сохранность гейта `Approved` на остальных стадиях
Статус `Approved` обязан продолжать работать как человеческий гейт:
- `analysis``architecture` (`check_analysis_approved`, approved-via-status);
- любой иной человеческий approve-advance, существующий сегодня.
Регрессия `handle_verdict(approved=True)` для НЕ-`deploy` стадий недопустима.
### TRZ-5. CTA Фазы A
Текст запроса approve в `_handle_self_deploy_phase_a` (Plane-комментарий + Telegram)
обязан инструктировать оператора переводить задачу в статус **`Confirm Deploy`**
(а не `Approved`) для запуска прод-деплоя.
### TRZ-6. Условность (как ORCH-35/36)
Ветка confirm-deploy реальна только для self-hosting
(`self_deploy.self_deploy_applies(repo)``orchestrator`). Для прочих репо —
прежнее поведение (синхронный деплой агентом), статус `Confirm Deploy` не
требуется и не влияет.
## 3. Изменения API
Изменений HTTP-эндпоинтов **нет**. Канал — существующий `POST /webhook/plane`
(событие `work_item.updated`). Внешнее изменение: в проекте ORCH появляется
дополнительный статус доски «Confirm Deploy» (Plane-конфигурация, не код-API).
## 4. Изменения схемы БД
**Нет.** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, таблицы `tasks`/`jobs`/
`agent_runs`/`events` — без изменений. Статусы — на стороне Plane; restart-safe
состояние деплоя — существующие sentinel-файлы ORCH-036 (без миграций).
## 5. Требования к новым QG checks
**Нет.** Новый Quality Gate не вводится. `check_deploy_status` /
`_parse_deploy_status` и контракт exit-кодов хука (0/1/2) — без изменений.
## 6. Конфигурация среды (предусловие эксплуатации)
- В проекте ORCH в Plane создаётся статус доски **«Confirm Deploy»** (точное имя,
чувствительно к регистру — должно совпасть с ключом `_PLANE_NAME_TO_KEY`).
- Размещение статуса на доске — рядом со стадией deploy/approval-pending
(рекомендация эксплуатации, не код).
- Кэш состояний (`get_project_states` / `reload_project_states`): после создания
статуса может потребоваться сброс кэша или рестарт по штатной стадии deploy.
## 7. Артефакты, создаваемые/обновляемые по pipeline
- `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` — решение
(как отличается триггер; где разрезается перегрузка `Approved`; fail-closed
при отсутствии статуса) — **ведёт архитектор**.
- `CLAUDE.md` — упоминание выделенного статуса approve прод-деплоя (раздел
self-hosting / артефакты).
- `docs/architecture/README.md` — секция ORCH-036: уточнить, что Фаза B
триггерится статусом `Confirm Deploy`, а не `Approved`.
- `CHANGELOG.md` — запись ORCH-059.
- `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`
штатно по стадиям конвейера.
## 8. Совместимость и инварианты
- Не меняются: `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`,
БАГ-8 (FAILED → откат на development), merge-gate, exit-коды хука, Фазы A/C,
схема БД, post-deploy (ORCH-021).
- Self-hosting safety: правка НЕ требует внепланового рестарта прод-контейнера;
выкат — через штатный deploy-staging (8501) → deploy.
- Never-crash: отсутствие статуса `Confirm Deploy` в резолвере не приводит к
исключению в webhook-пути.

View File

@@ -0,0 +1,76 @@
# 03 — Критерии приёмки: ORCH-059
Repo: `orchestrator` · Stage: analysis
Каждый критерий — однозначный PASS/FAIL. Проверка: unit/integration (см.
`04-test-plan.yaml`) + ручная верификация для инфра-предусловий.
## AC-1 — Статус «Confirm Deploy» резолвится
**Given** проект ORCH со статусом доски «Confirm Deploy»
**When** вызывается резолвер состояний для проекта ORCH
**Then** возвращается логический ключ `confirm_deploy` с непустым UUID,
а маппинг `"Confirm Deploy" → "confirm_deploy"` присутствует в `_PLANE_NAME_TO_KEY`.
**FAIL:** ключ отсутствует или указывает на UUID статуса `Approved`.
## AC-2 — «Confirm Deploy» на стадии `deploy` запускает Фазу B
**Given** задача self-hosting (`orchestrator`) на стадии `deploy`,
`deploy_require_manual_approve=true`, маркер `initiated` отсутствует
**When** приходит `work_item.updated` со статусом `Confirm Deploy`
**Then** инициируется Фаза B: вызывается `self_deploy.initiate_deploy`,
ставится job `deploy-finalizer`, пишется маркер `initiated`.
**FAIL:** прод-деплой не инициирован, либо finalizer не поставлен.
## AC-3 — «Approved» на стадии `deploy` НЕ запускает прод-деплой
**Given** та же задача на стадии `deploy`
**When** приходит `work_item.updated` со статусом `Approved`
**Then** `self_deploy.initiate_deploy` **НЕ** вызывается; Фаза B не стартует;
задача не откатывается (БАГ-8 не срабатывает) и не «доходит» по
`check_deploy_status` (вердикта нет); событие залогировано как no-op.
**FAIL:** вызван `initiate_deploy`, либо произошёл откат/ложный advance.
## AC-4 — «Approved» на `analysis` работает без регрессии
**Given** задача на стадии `analysis` (BRD готов, approval-pending)
**When** issue переводится в `Approved`
**Then** срабатывает approved-via-status и задача продвигается
`analysis → architecture` (как до правки).
**FAIL:** approve на analysis перестал продвигать конвейер.
## AC-5 — Идемпотентность Фазы B по «Confirm Deploy»
**Given** задача на `deploy`, маркер `initiated` уже существует
**When** повторно приходит статус `Confirm Deploy` (двойной клик / дубль webhook)
**Then** повторного `initiate_deploy` не происходит (no-op,
`self-deploy-already-initiated`).
**FAIL:** прод-деплой запускается повторно.
## AC-6 — CTA Фазы A просит «Confirm Deploy»
**Given** Фаза A (`deploy-staging → deploy`, approval-pending)
**When** формируются Plane-комментарий и Telegram-уведомление запроса approve
**Then** текст инструктирует перевести задачу в статус **`Confirm Deploy`**
(а не «Approved») для запуска прод-деплоя.
**FAIL:** CTA по-прежнему упоминает только «Approved».
## AC-7 — Fail-closed при отсутствии статуса
**Given** среда без статуса «Confirm Deploy» (enduro / fallback `_DEFAULT_STATES`
/ недоступный Plane API)
**When** обрабатывается `work_item.updated`
**Then** webhook-путь не выбрасывает исключение; ветка confirm-deploy не
активируется (прод-деплой не запускается «вслепую»).
**FAIL:** `KeyError`/исключение в обработчике, либо ложный запуск Фазы B.
## AC-8 — Условность для не-self репозиториев
**Given** не-self репозиторий (`self_deploy_applies(repo) == False`)
**When** приходит любой verdict-статус на стадии `deploy`
**Then** поведение прод-деплоя не меняется относительно текущего (синхронный
деплой агентом); статус `Confirm Deploy` не требуется.
**FAIL:** изменилось поведение деплоя не-self проекта.
## AC-9 — Инварианты не нарушены
**Then** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/
`_parse_deploy_status`, контракт exit-кодов хука (0/1/2), Фазы A/C, merge-gate,
схема БД — без изменений; `pytest tests/ -q` зелёный.
**FAIL:** изменён любой из перечисленных контрактов или красные тесты.
## AC-10 — Документация обновлена (golden source)
**Then** в том же PR обновлены `CLAUDE.md`, секция ORCH-036 в
`docs/architecture/README.md`, `CHANGELOG.md`; заведён
`06-adr/ADR-001-confirm-deploy-status.md`.
**FAIL:** функционал изменён, документация — нет (Reviewer → REQUEST_CHANGES).

View File

@@ -0,0 +1,109 @@
work_item: ORCH-059
title: Approve прод-деплоя через выделенный статус «Confirm Deploy»
repo: orchestrator
stage: analysis
# Контракт-тесты: триггер прод-деплоя смещается с перегруженного `Approved`
# на выделенный статус `Confirm Deploy`. Деплой и сетевые вызовы мокаются.
tests:
- id: TC-01
type: unit
description: "_PLANE_NAME_TO_KEY содержит маппинг 'Confirm Deploy' -> 'confirm_deploy'"
module: tests/test_plane_states.py
expected: PASS
- id: TC-02
type: unit
description: >-
get_project_states для проекта ORCH (мок API со статусом 'Confirm Deploy')
возвращает непустой UUID под ключом 'confirm_deploy', отличный от 'approved'
module: tests/test_plane_states.py
expected: PASS
- id: TC-03
type: unit
description: >-
Fail-closed: при отсутствии статуса 'Confirm Deploy' (fallback _DEFAULT_STATES /
недоступный API) доступ к ключу confirm_deploy не выбрасывает исключение
и не активирует ветку confirm-deploy
module: tests/test_plane_states.py
expected: PASS
- id: TC-04
type: unit
description: >-
handle_issue_updated: статус 'Confirm Deploy' на задаче стадии deploy
маршрутизируется на путь Фазы B (а не на обычный approve/advance)
module: tests/test_plane_confirm_deploy.py
expected: PASS
- id: TC-05
type: unit
description: >-
handle_verdict/Approved на стадии deploy НЕ вызывает self_deploy.initiate_deploy
(initiate_deploy замокан и не должен быть вызван)
module: tests/test_plane_confirm_deploy.py
expected: PASS
- id: TC-06
type: unit
description: >-
Approved на стадии analysis по-прежнему продвигает analysis -> architecture
(approved-via-status, регрессия гейта check_analysis_approved)
module: tests/test_plane_confirm_deploy.py
expected: PASS
- id: TC-07
type: unit
description: >-
stage_engine: блок Фазы B (current_stage==deploy, finished_agent is None)
инициирует deploy ТОЛЬКО по сигналу confirm-deploy; Approved-сигнал -> no-op
module: tests/test_stage_engine_phase_b.py
expected: PASS
- id: TC-08
type: unit
description: >-
Идемпотентность: при существующем маркере 'initiated' повторный
Confirm Deploy не вызывает initiate_deploy (self-deploy-already-initiated)
module: tests/test_stage_engine_phase_b.py
expected: PASS
- id: TC-09
type: unit
description: >-
CTA Фазы A (_handle_self_deploy_phase_a): текст Plane-комментария и Telegram
содержат 'Confirm Deploy' и не предлагают 'Approved' как триггер деплоя
module: tests/test_stage_engine_phase_a_cta.py
expected: PASS
- id: TC-10
type: integration
description: >-
E2E (мок Plane API + self_deploy): задача на deploy -> webhook Confirm Deploy
-> initiate_deploy вызван, deploy-finalizer поставлен, маркер initiated записан
module: tests/test_confirm_deploy_integration.py
expected: PASS
- id: TC-11
type: integration
description: >-
E2E: задача на deploy -> webhook Approved -> прод-деплой НЕ инициирован,
задача остаётся на deploy (нет отката, нет advance в done)
module: tests/test_confirm_deploy_integration.py
expected: PASS
- id: TC-12
type: integration
description: >-
Условность: для не-self репозитория verdict-статусы на deploy не меняют
поведение деплоя (self_deploy_applies == False)
module: tests/test_confirm_deploy_integration.py
expected: PASS
regression:
- id: RG-01
type: integration
description: "pytest tests/ -q зелёный; STAGE_TRANSITIONS и QG_CHECKS без изменений"
module: tests/
expected: PASS

View File

@@ -0,0 +1,156 @@
# ADR-001 (ORCH-059): Выделенный статус «Confirm Deploy» как триггер прод-деплоя
## Статус
Accepted (design) — реализация в ветке `feature/ORCH-059-approve-confirm-deploy-approve`.
## Контекст
ORCH-036 (исполняемый самодеплой стадии `deploy`) запускает прод-деплой
self-hosting инстанса **Фазой B**: человек переводит issue в Plane-статус
`Approved` → webhook `work_item.updated``handle_issue_updated`
`handle_verdict(approved=True)``_try_advance_stage`
`advance_stage(finished_agent=None)`; в `stage_engine.advance_stage` блок
`current_stage == "deploy" and finished_agent is None`
`_handle_self_deploy_phase_b` → detached host-деплой прода (8500).
Тот же UUID `Approved` (`a519a341-…`, `_DEFAULT_STATES["approved"]`) — это
**человеческий гейт одобрения** на стадии `analysis`
(`check_analysis_approved`, путь `approved-via-status`) и общий verdict-роутинг
в `handle_verdict`. Один визуальный «Approved» на доске значит две принципиально
разные вещи: «принять BRD» (дёшево, обратимо) и «**ВЫКАТИТЬ В ПРОД** инструмент,
обслуживающий все проекты из одного инстанса с общей БД» (дорого, групповой
риск). Привычный жест approve на стадии `deploy` молча триггерит прод-рестарт —
цена случайного клика высока (см. self-hosting в `CLAUDE.md`).
Ограничения, формирующие дизайн (см. `02-trz.md`, `03-acceptance-criteria.md`):
1. **Нулевая регрессия** гейта `Approved` на `analysis` и прочих стадиях (TRZ-4).
2. **Fail-closed**: среды без статуса (enduro, fallback `_DEFAULT_STATES`,
недоступный API) не должны падать и не должны «вслепую» деплоить (TRZ-1, R-1).
3. **`Approved` на `deploy` не должен** запускать Фазу B И не должен вызывать
ложный откат (БАГ-8) или ложный advance по `check_deploy_status` — вердикта
ещё нет (TRZ-3, R-2).
4. **Без правки контрактов**: `STAGE_TRANSITIONS`, `QG_CHECKS`,
`check_deploy_status`, Фазы A/C, merge-gate, exit-коды хука, схема БД (TRZ-8).
5. **Self-hosting safety**: правка — чистая маршрутизация, не требует внепланового
рестарта прода; выкат через штатный `deploy-staging` (8501) → `deploy` (R-3).
## Решение
Ввести отдельный логический статус `confirm_deploy` («Confirm Deploy»), который
триггерит **ТОЛЬКО** Фазу B на стадии `deploy`. `Approved` теряет смысл «запусти
прод-деплой» и остаётся исключительно человеческим гейтом конвейера.
Четыре точечные правки в трёх модулях:
### 1. Резолвер состояний — `src/plane_sync.py`
- В `_PLANE_NAME_TO_KEY` добавить маппинг `"Confirm Deploy" → "confirm_deploy"`.
- В `_DEFAULT_STATES` ключ `confirm_deploy` **НЕ добавлять** (реального UUID для
enduro/fallback нет; отсутствие ключа = fail-closed). Для проекта ORCH ключ
резолвится `get_project_states` из живого Plane API; для проектов без статуса и
на fallback-пути ключ просто отсутствует в результирующем словаре.
- Следствие: `get_project_states(orch)["confirm_deploy"]` → реальный UUID;
`get_project_states(enduro).get("confirm_deploy")``None`.
### 2. Маршрутизация webhook — `src/webhooks/plane.py`
В `handle_issue_updated`, **до** ветки `approved`, добавить fail-closed-ветку:
```python
confirm_state = proj_states.get("confirm_deploy") # .get -> AC-7/R-1
if confirm_state and new_state == confirm_state:
await handle_confirm_deploy(data, project_id)
elif new_state == proj_states["in_progress"]:
...
elif new_state == proj_states["approved"]:
await handle_verdict(data, project_id, approved=True)
```
Новый `handle_confirm_deploy(data, project_id)`:
- резолвит задачу по `plane_id`;
- если `stage != "deploy"`**no-op с логом** (Confirm Deploy осмыслен только на
approval-pending стадии `deploy`; защищает прочие гейты от случайного approve);
- иначе → `_try_advance_stage(..., confirm_deploy=True)`.
`handle_verdict(approved=True)` не меняется — продолжает звать `_try_advance_stage`
с `confirm_deploy=False` (дефолт).
### 3. Сигнал в движок — `src/stage_engine.advance_stage(...)`
Добавить keyword-only параметр `confirm_deploy: bool = False` (back-compat: все
существующие вызовы из launcher/reconciler/finalizer/webhook передают
`finished_agent`, новый kwarg дефолтный). Блок Фазы B переписать так, чтобы он
**всегда возвращался рано** для `deploy + finished_agent is None` self-hosting,
но деплоил только по сигналу:
```python
if (current_stage == "deploy" and finished_agent is None
and settings.deploy_require_manual_approve
and self_deploy.self_deploy_applies(repo)):
if confirm_deploy:
_handle_self_deploy_phase_b(task_id, repo, work_item_id, branch, result)
else:
# TRZ-3/R-2: обычный Approved на deploy — no-op; НЕ запускаем
# check_deploy_status (вердикта ещё нет -> ложный откат БАГ-8).
result.note = "approved-on-deploy-noop"
return result
```
Ключевое: возврат **до** блока Quality Gate в обоих случаях → `check_deploy_status`
по `Approved` на `deploy` не исполняется. Фаза C (finalizer,
`finished_agent="deployer"`) не затронута — условие требует `finished_agent is
None`.
### 4. CTA Фазы A — `src/stage_engine._handle_self_deploy_phase_a`
Текст Plane-комментария и Telegram изменить: вместо «смените статус на Approved»
инструктировать перевести задачу в статус **«Confirm Deploy»** для запуска
прод-деплоя (TRZ-5/AC-6).
### Условность (как ORCH-35/36)
Вся ветка реальна только для `self_deploy.self_deploy_applies(repo)`
`orchestrator`. Прочие репо — прежний синхронный ssh-деплой агентом; статус
`Confirm Deploy` им не нужен и на них не влияет (AC-8).
## Альтернативы
- **A. Telegram inline-кнопка подтверждения** вместо нового статуса — отклонено:
кнопочная инфраструктура в коде отсутствует, заявлено вне scope (ORCH-036 п.
«inline-кнопка» не реализован); управление остаётся статусом Plane.
- **B. Добавить `confirm_deploy` в `_DEFAULT_STATES`** — отклонено: реального UUID
«Confirm Deploy» для enduro/fallback нет; пришлось бы подставить фиктивный или
дублирующий UUID, что ломает fail-closed (enduro «получил бы» триггер деплоя) и
смешивает семантику.
- **C. Отдельный публичный entrypoint `stage_engine.initiate_confirm_deploy()`**,
минующий `advance_stage` — отклонено: дублирует гарды
(`deploy_require_manual_approve`, `self_deploy_applies`, idempotency `initiated`),
и всё равно пришлось бы внутри `advance_stage` гасить `Approved`-на-`deploy` в
no-op. Параметр-сигнал проще и держит единую точку правды.
- **D. Сигнал через sentinel-маркер, записываемый webhookом** — отклонено: вызов
синхронный в пределах одного `advance_stage`, persistence не нужна; параметр
явнее и не плодит файловое состояние.
## Последствия
**Плюсы**
- Жест «запустить прод-деплой» отделён от «одобрить артефакт»; случайный approve
на доске больше не роняет прод (BG-1, BG-2).
- `Approved` на `deploy` детерминированно безопасен: no-op без отката/advance
(закрывает R-2).
- Fail-closed: нет статуса → нет деплоя, нет исключения (R-1, AC-7).
- Минимальный диффузный риск: контракты `STAGE_TRANSITIONS`/`QG_CHECKS`/
`check_deploy_status`/Фазы A/C/merge-gate/схема БД не тронуты (AC-9).
- Реконсилятор F-1 на `deploy` (finished_agent=None) теперь попадает в no-op-ветку
вместо прежнего неявного запуска Фазы B → прод-деплой невозможно инициировать
автоматически, только явным человеческим `Confirm Deploy` (усиление safety).
**Минусы / цена**
- Эксплуатационное предусловие: в Plane-проекте ORCH нужно создать статус доски
«Confirm Deploy» (точное имя, регистр) и сбросить кэш состояний — см.
`07-infra-requirements.md`. До создания статуса прод-деплой через approve не
запустится (это и есть желаемое fail-closed-поведение).
- Сигнатура `advance_stage` расширена одним kwarg (обратносовместимо).
**Хэндофф документации (golden source, в том же PR — стадия development).**
ADR (этот файл) — артефакт архитектора. Переписать `Approve = Approved`
`Confirm Deploy` в `docs/architecture/README.md` (секция ORCH-036), `CLAUDE.md`
(self-hosting/артефакты) и добавить запись в `CHANGELOG.md` обязан developer
одновременно с кодом (AC-10), чтобы доки не описывали ещё не существующее
поведение. В README на стадии architecture добавлена forward-looking пометка
ORCH-059 (design), как принято для незамёрженных доработок.
## Связанные ADR
- `adr-0007-executable-self-deploy.md` (ORCH-036) — задаёт Фазы A/B/C; ORCH-059
меняет **только триггер** Фазы B (`Approved``Confirm Deploy`) и делает
`Approved`-на-`deploy` no-op; Фазы внутренне не меняются.
- `adr-0003-staging-gate.md` (ORCH-35) — паттерн условности self-hosting.
- `adr-0007-reconciler.md` (ORCH-053) — реконсилятор F-1: поведение на `deploy`
становится no-op (см. Последствия).

View File

@@ -0,0 +1,44 @@
# 07 — Требования к инфраструктуре: ORCH-059
Work Item: **ORCH-059** · Repo: `orchestrator`
Связано: `06-adr/ADR-001-confirm-deploy-status.md`, `02-trz.md` §6.
> Топология контейнеров/портов/деплоя НЕ меняется (см. `docs/operations/INFRA.md`).
> Единственное инфра-требование ORCH-059 — конфигурация Plane-доски проекта ORCH.
## IR-1. Статус доски «Confirm Deploy» в проекте ORCH (предусловие эксплуатации)
- В Plane-проекте **ORCH** создать кастомный статус доски с **точным** именем
`Confirm Deploy` (case-sensitive, ровно один пробел) — должно посимвольно
совпасть с ключом `_PLANE_NAME_TO_KEY["Confirm Deploy"]`. Несовпадение →
fail-closed (деплой не запустится), не краш (R-9).
- UUID статуса генерирует Plane; код резолвит его через `get_project_states`
(`GET /workspaces/<ws>/projects/<orch>/states/`). Хардкодить UUID не нужно.
- **Размещение** на доске — рядом с approval-pending/`deploy` (рекомендация
эксплуатации, на поведение кода не влияет).
- **Только проект ORCH** (self-hosting). Для enduro и прочих проектов статус НЕ
создаётся и НЕ требуется — `self_deploy_applies` истинно лишь для `orchestrator`.
## IR-2. Сброс кэша состояний после создания статуса
`get_project_states` кэширует резолв per-project на время жизни процесса
(`_STATES_CACHE`). После создания статуса в Plane закэшированный словарь не
содержит `confirm_deploy` (R-5). Применить ОДНО из:
- вызвать `reload_project_states(<orch_project_id>)` (или полный сброс), либо
- штатно перезапустить прод по конвейеру `deploy-staging → deploy` (рестарт
процесса очищает кэш).
> Внеплановый ручной рестарт прод-контейнера для применения этой задачи **не
> требуется** и противопоказан (self-hosting групповой риск). Выкат — только через
> штатный staging→deploy.
## IR-3. Контрольная проверка готовности среды
После IR-1+IR-2:
1. `get_project_states(<orch>)` содержит `confirm_deploy` с непустым UUID,
отличным от `approved` (AC-1, TC-02).
2. Перевод тестовой задачи стадии `deploy` (sandbox) в `Confirm Deploy` запускает
Фазу B; перевод в `Approved` — нет (AC-2/AC-3).
## Что НЕ меняется
- Порты (8500 prod / 8501 staging), контейнеры, compose-профили, env-карта,
деплой-хук, схема БД, sentinel-каталоги ORCH-036 — без изменений.
- HTTP-эндпоинты (`POST /webhook/plane` тот же канал, событие
`work_item.updated`).

View File

@@ -0,0 +1,25 @@
# 10 — Технические риски: ORCH-059
Work Item: **ORCH-059** · Repo: `orchestrator` · ведёт: архитектор
Связано: `06-adr/ADR-001-confirm-deploy-status.md`.
| ID | Риск | Вероятн. | Влияние | Митигация | Проверка |
|----|------|----------|---------|-----------|----------|
| R-1 | Ключ `confirm_deploy` отсутствует в `_DEFAULT_STATES` / у проектов без статуса → `KeyError` в webhook-пути | Сред | Выс (краш обработчика) | Доступ ТОЛЬКО через `.get("confirm_deploy")`; `_DEFAULT_STATES` не содержит ключ намеренно; отсутствие → ветка не активируется (fail-closed) | TC-03, AC-7 |
| R-2 | `Approved` на `deploy` после правки вызывает `check_deploy_status` (вердикта нет) → ложный откат БАГ-8 / ложный advance | Выс | Выс (петля dev↔deploy, ложный rollback прода) | Блок Фазы B возвращается рано для `deploy + finished_agent is None` self-hosting в ОБОИХ случаях; `Approved``note=approved-on-deploy-noop`, QG не запускается | TC-05, TC-07, TC-11, AC-3 |
| R-3 | Самоправка прода требует внепланового рестарта прод-контейнера | Низ | Выс (встаёт конвейер всех проектов) | Изменение — чистая маршрутизация в коде; выкат через штатный `deploy-staging` (8501) → `deploy`; sentinel-состояние ORCH-036 не трогаем | AC-9, RG-01 |
| R-4 | `Confirm Deploy` прислан на не-`deploy` стадии (оператор ошибся) → срабатывает как обычный approve и продвигает чужой гейт | Низ | Сред | `handle_confirm_deploy` гардит `stage == "deploy"`; иначе no-op с логом | TC-04 (+ ручная верификация) |
| R-5 | Кэш `get_project_states` закэширован до создания статуса «Confirm Deploy» → ключ не виден после конфигурации Plane | Сред | Сред (деплой не запускается) | После создания статуса в Plane — `reload_project_states(orch)` или штатный рестарт по стадии `deploy`; зафиксировано в `07-infra-requirements.md` | ручная верификация |
| R-6 | Новый kwarg `confirm_deploy` ломает существующие вызовы `advance_stage` (launcher/reconciler/finalizer) | Низ | Выс | keyword-only с дефолтом `False`; все вызовы передают `finished_agent`; не-`deploy`/finished_agent≠None пути не затронуты | RG-01, AC-9 |
| R-7 | Регрессия идемпотентности Фазы B (двойной `Confirm Deploy`) | Низ | Сред | Внутренности `_handle_self_deploy_phase_b` (маркер `initiated`) не меняются; меняется только триггер | TC-08, AC-5 |
| R-8 | Реконсилятор F-1 на `deploy` (finished_agent=None) меняет поведение | Низ | Низ (улучшение) | Намеренно: раньше неявно мог войти в Фазу B, теперь → no-op. Прод-деплой инициируется только явным `Confirm Deploy`. Документировано в ADR/README | RG-01 |
| R-9 | Несовпадение имени статуса в Plane и `_PLANE_NAME_TO_KEY` (регистр/пробел) → ключ не резолвится | Сред | Сред (деплой не запускается, fail-closed) | Точное имя «Confirm Deploy» (case-sensitive) — требование среды в `07-infra-requirements.md`; маппинг ровно этой строкой | TC-01, TC-02 |
## Сводный вывод
Все риски — низкого/среднего остаточного уровня после митигаций. Доминирующий
класс — **fail-closed**: любая неполнота конфигурации (нет статуса, протухший кэш,
недоступный API) приводит к «деплой не запускается», а не к «деплой запускается
вслепую» или к крашу. Контракты конвейера (`STAGE_TRANSITIONS`, `QG_CHECKS`,
`check_deploy_status`, Фазы A/C, merge-gate, схема БД) не затрагиваются, поэтому
поверхность регрессии ограничена тремя модулями (`plane_sync.py`,
`webhooks/plane.py`, `stage_engine.py`).

View File

@@ -0,0 +1,59 @@
---
type: review
work_item_id: ORCH-059
verdict: APPROVED
version: 1
---
# Review ORCH-059
## Summary
Выделенный Plane-статус «Confirm Deploy» как единственный триггер Фазы B прод-деплоя
self-hosting; `Approved` на стадии `deploy` становится детерминированным no-op. Реализация
точно соответствует ТЗ (TRZ-1..6), ADR-001 и критериям приёмки (AC-1..10). Четыре точечные
правки в трёх модулях (`plane_sync.py`, `webhooks/plane.py`, `stage_engine.py`), без изменения
контрактов (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, Фазы A/C, merge-gate, схема
БД). Документация обновлена в том же PR. `pytest tests/ -q` — 763 passed.
## Соответствие ТЗ и ADR
- **TRZ-1 / AC-1** — `"Confirm Deploy" → "confirm_deploy"` добавлен в `_PLANE_NAME_TO_KEY`;
намеренно отсутствует в `_DEFAULT_STATES` → fail-closed. Покрыто `test_tc01/tc02`.
- **TRZ-2 / AC-2** — `handle_confirm_deploy` (гард `stage=="deploy"`) →
`_try_advance_stage(..., confirm_deploy=True)` → Фаза B. Покрыто `test_tc04/tc07/tc10`.
- **TRZ-3 / AC-3** — `Approved` на `deploy`: ранний возврат ДО Quality Gate с
`note="approved-on-deploy-noop"`, без `initiate_deploy`, без ложного отката БАГ-8.
Покрыто `test_tc05/tc07_approved_without_confirm_is_noop/tc11`.
- **TRZ-4 / AC-4** — `handle_verdict(approved=True)` не тронут; approve на `analysis`
продвигает конвейер. Покрыто `test_tc06_approved_on_analysis_still_advances`.
- **AC-5** — идемпотентность повторного «Confirm Deploy» (`self-deploy-already-initiated`).
Покрыто `test_tc08`, `test_tc06_approved_calls_prod_hook_exactly_once`.
- **TRZ-5 / AC-6** — CTA Фазы A (Plane-коммент + Telegram) просит «Confirm Deploy» и явно
отмечает, что «Approved» прод-деплой не запускает. Покрыто `test_tc09`.
- **TRZ-1 / AC-7** — доступ через `.get("confirm_deploy")`, отсутствие статуса → ветка не
активируется, без `KeyError`. Покрыто `test_tc03` (API недоступен / статуса нет на доске).
- **TRZ-6 / AC-8** — условность через `self_deploy.self_deploy_applies`; не-self репо без
изменений. Покрыто `test_tc12`.
- **AC-9** — контракты и схема БД не изменены; 763 теста зелёные.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
## Документация
Обновлено в том же PR (AC-10 выполнен):
- `CLAUDE.md` — раздел self-hosting: прод-деплой только через «Confirm Deploy», `Approved` = no-op.
- `docs/architecture/README.md` — секция ORCH-036 уточнена + добавлена подсекция ORCH-059
(статус-триггер «Confirm Deploy»), запись в перечне статусов доработок.
- `CHANGELOG.md` — запись ORCH-059 в `[Unreleased] / Added`.
- ADR `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` — заведён, отражает
реализацию (4 правки, fail-closed, рассмотренные альтернативы).
- `07-infra-requirements.md` — эксплуатационное предусловие (создать статус доски + сброс кэша).
Документация консистентна с кодом; golden-source инвариант соблюдён.

View File

@@ -0,0 +1,71 @@
---
type: test-report
work_item_id: ORCH-059
result: PASS
---
# Test Report — ORCH-059
Выделенный Plane-статус «Confirm Deploy» как единственный триггер Фазы B прод-деплоя
self-hosting; `Approved` на стадии `deploy` — детерминированный no-op.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Prod orchestrator (8500): `/health``{"status":"ok"}`
- Дата: 2026-06-07
## Результаты (контракт-тесты `04-test-plan.yaml`)
| TC ID | Описание | Тест | Результат |
|-------|----------|------|-----------|
| TC-01 | `_PLANE_NAME_TO_KEY`: `'Confirm Deploy' → 'confirm_deploy'` | test_tc01_confirm_deploy_name_to_key_mapping; test_tc01_confirm_deploy_not_in_default_states | PASS |
| TC-02 | `get_project_states` ORCH резолвит непустой UUID под `confirm_deploy`, ≠ `approved` | test_tc02_get_project_states_resolves_confirm_deploy | PASS |
| TC-03 | Fail-closed при отсутствии статуса (API недоступен / нет на доске) — без исключения | test_tc03_fail_closed_when_api_unreachable; test_tc03_fail_closed_when_status_not_on_board | PASS |
| TC-04 | `handle_issue_updated`: `Confirm Deploy` на `deploy` → путь Фазы B | test_tc04_confirm_deploy_routes_phase_b; test_tc04b_confirm_deploy_off_deploy_stage_is_noop | PASS |
| TC-05 | `Approved` на `deploy` НЕ вызывает `initiate_deploy` | test_tc05_approved_on_deploy_does_not_initiate | PASS |
| TC-06 | `Approved` на `analysis` по-прежнему продвигает → architecture | test_tc06_approved_on_analysis_still_advances | PASS |
| TC-07 | stage_engine: Фаза B только по confirm-deploy; `Approved` → no-op | test_tc07_confirm_deploy_initiates; test_tc07_approved_without_confirm_is_noop | PASS |
| TC-08 | Идемпотентность: повтор `Confirm Deploy` при маркере `initiated` → no-op | test_tc08_idempotent_repeat_confirm_deploy | PASS |
| TC-09 | CTA Фазы A содержит «Confirm Deploy», не предлагает «Approved» как триггер | test_tc09_phase_a_cta_requests_confirm_deploy | PASS |
| TC-10 | E2E: `Confirm Deploy``initiate_deploy` вызван, finalizer поставлен, маркер записан | test_tc10_confirm_deploy_e2e_initiates | PASS |
| TC-11 | E2E: `Approved` → деплой НЕ инициирован, задача остаётся на `deploy` | test_tc11_approved_e2e_noop | PASS |
| TC-12 | Условность: не-self репо verdict-статусы не меняют поведение деплоя | test_tc12_non_self_repo_unaffected | PASS |
| RG-01 | Полный регресс зелёный; STAGE_TRANSITIONS / QG_CHECKS без изменений | tests/ (763 passed) | PASS |
Все 16 целевых тестов ORCH-059 (TC-01..TC-12) — PASS.
## Сопоставление с критериями приёмки (`03-acceptance-criteria.md`)
| AC | Покрытие | Результат |
|----|----------|-----------|
| AC-1 Статус резолвится | TC-01, TC-02 | PASS |
| AC-2 Confirm Deploy на `deploy` → Фаза B | TC-04, TC-07, TC-10 | PASS |
| AC-3 Approved на `deploy` НЕ деплоит | TC-05, TC-07, TC-11 | PASS |
| AC-4 Approved на `analysis` без регрессии | TC-06 | PASS |
| AC-5 Идемпотентность Фазы B | TC-08 | PASS |
| AC-6 CTA Фазы A просит Confirm Deploy | TC-09 | PASS |
| AC-7 Fail-closed без статуса | TC-03 | PASS |
| AC-8 Условность для не-self | TC-12 | PASS |
| AC-9 Инварианты, pytest зелёный | RG-01 (763 passed) | PASS |
| AC-10 Документация обновлена | проверено reviewer (12-review.md, APPROVED) | PASS |
## Smoke test API (prod 8500)
- `GET /health``{"status":"ok","service":"orchestrator"}`
- `GET /status` → 200, активные задачи отдаются (вкл. ORCH-059 на `testing`)
- `GET /queue` → 200, counts + resilience + reconcile + reaper + post_deploy
## Вывод pytest
```
======================= 763 passed, 1 warning in 15.45s ========================
```
Целевой набор ORCH-059:
```
======================== 16 passed, 1 warning in 0.75s =========================
```
(1 warning — PydanticDeprecatedSince20 в `src/config.py`, не относится к ORCH-059.)
## Итог
**PASS** — все контракт-тесты (TC-01..TC-12) и регресс (763 passed) зелёные,
критерии приёмки AC-1..AC-10 покрыты, smoke API OK. Задача готова к стадии
deploy-staging.

View File

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

View File

@@ -0,0 +1,29 @@
---
staging_status: SUCCESS
timestamp: 2026-06-07T19:19:25Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. Verdict: **SUCCESS** (exit 0).
Canonical run inside the `orchestrator-staging` container (ORCH-048, ADR-001):
`python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
## Result
- RESULT: 8/10 checks PASS
- REAL failed: none
- SANDBOX_INFRA failed: C9a (branch in orchestrator-sandbox), C9b (analyst job enqueued)
All REAL pipeline checks (Block A SMOKE, Block B ACCESS incl. B6 registry isolation,
C7/C8) are green. The two failing checks are sandbox-infra-only (SANDBOX bot accounts
not members of the SANDBOX Plane project) and were waived per ORCH-061. Exit code 0.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
tolerance: staging_infra_tolerance_enabled=True

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-059
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.

View File

@@ -0,0 +1,7 @@
# Business Request: [высокий] Статусная модель Plane: осмысленные статусы этапов
Work Item ID: ORCH-066
## Description
TBD

View File

@@ -0,0 +1,110 @@
# 01 — Business Requirements Document (BRD)
**Work Item:** ORCH-066
**Заголовок:** [высокий] Статусная модель Plane: осмысленные статусы этапов
**Стадия:** analysis
**Автор:** Analyst
**Дата:** 2026-06-07
---
## 1. Контекст и проблема
Статусная модель Plane оркестратора имеет **семантические перегрузки**: один и тот
же Plane-статус используется для несовместимых смыслов, из-за чего:
- оператор не понимает, на каком реально этапе стоит задача (доска нечитаема);
- повышается риск ошибки оператора (например, неверный ручной перевод статуса);
- `In Progress` одновременно означает «человек запускает конвейер», «идёт анализ»,
«идёт прод-деплой» и «возврат из Needs Input» — четыре разных смысла на одном статусе.
Уже частично исправлено: ORCH-059 ввёл отдельный статус для подтверждения деплоя
(`Confirm Deploy`), разгрузив перегруженный `Approved`. ORCH-066 завершает наведение
порядка по **утверждённой Owner** статусной модели.
### Два слоя (критично различать)
| Слой | Что это | Источник | Трогаем? |
|------|---------|----------|----------|
| **A** | `STAGE_TRANSITIONS` — внутренняя машина стадий (`created→analysis→…→done`) | `src/stages.py` | **НЕТ (инвариант)** |
| **B** | Plane-статусы — индикация на доске | `src/plane_sync.py` + точки в `src/stage_engine.py` / `src/webhooks/plane.py` | **ДА** |
ORCH-066 меняет **только слой B** и точки, где код вручную проставляет Plane-статусы.
---
## 2. Целевая статусная модель (решение Owner)
```
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
Monitoring after Deploy → Done
```
- `[...]` = **действие человека** (вход-триггер).
- Остальное ставит **орк** (индикация).
### Ветки (нелинейные исходы)
- **Rejected** — откат на предыдущую стадию (человек).
- **Needs Input** — ТОЛЬКО аналитик (НЕ расширять на других агентов).
- **Blocked** — затык / фейл деплоя / деградация прода.
- **Cancelled** — человек решил не делать задачу (валидный выход из In Review).
---
## 3. Бизнес-требования
| ID | Требование | Приоритет |
|----|------------|-----------|
| **BR-1** | Каждый этап конвейера показывается на доске Plane осмысленным статусом (To Analyse / Analysis / Code-Review / Awaiting Deploy / Deploying / Monitoring after Deploy). | Must |
| **BR-2** | `To Analyse` — единый человеческий вход: (а) старт нового конвейера, (б) resume/relaunch аналитика при возврате из Needs Input. Заменяет роль `In Progress` как входа-триггера. | Must |
| **BR-3** | Стадия `analysis` индицируется отдельным статусом `Analysis` (орк ставит при старте/relaunch аналитика), а не `In Progress`. | Must |
| **BR-4** | Стадия `review` индицируется Plane-статусом `Code-Review` (переименование `Review`). | Must |
| **BR-5** | Self-deploy Phase A (approval-pending) ставит `Awaiting Deploy` вместо `In Review`. | Must |
| **BR-6** | Self-deploy Phase B (старт прод-деплоя) ставит `Deploying`. | Must |
| **BR-7** | Self-deploy Phase C (health-OK финализация) ставит `Monitoring after Deploy` (НЕ `Done` сразу). | Must |
| **BR-8** | Post-deploy monitor (ORCH-021): чистое закрытие окна (HEALTHY) → `Done`; UNHEALTHY/деградация → `Blocked`. | Must |
| **BR-9** | `In Review` разгрузить: оставить ТОЛЬКО за approve-pending артефактов конвейера (BRD/ревью). Выходы: `Approved` (вперёд), `Rejected` (откат), `Cancelled` (человек отменил). | Must |
| **BR-10** | `Needs Input` — БЕЗ ИЗМЕНЕНИЙ. Остаётся только у аналитика (`01-questions.md``set_issue_needs_input`). Механизм не трогать. | Must |
| **BR-11** | Возврат аналитика из Needs Input выполняется через `To Analyse` (а НЕ через `In Progress`). Логика fork «старт vs resume» (по наличию task + active-job) сохраняется. | Must (грабли R1) |
| **BR-12** | **Fail-closed:** отсутствие нового статуса в проекте (enduro / Plane API down / fallback `_DEFAULT_STATES`) НЕ приводит к падению; поведение остаётся backward-compatible (паттерн ORCH-059 AC-7). | Must |
| **BR-13** | Reconciler не «оживляет» активные ожидания (`Awaiting Deploy` / `Deploying` / `Monitoring after Deploy`) как зависшие задачи (Guard 2 skip-list). | Must |
| **BR-14** | Документация (golden source) обновлена в том же PR: `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`, ADR per-work-item. | Must |
---
## 4. Границы (Out of Scope / НЕ трогать)
- `STAGE_TRANSITIONS` (`src/stages.py`) — машина стадий, инвариант.
- `QG_CHECKS`, `check_deploy_status`, exit-коды хука (0/1/2), merge-gate, схема БД.
- `Confirm Deploy` (уже работает, ORCH-059).
- Механизм `Needs Input` (analyst-only) — не расширять, не менять.
- Поведение прод-деплоя **не-self** репозиториев (enduro-trails): для них терминальный
переход остаётся `deploy → Done` как сейчас (Monitoring after Deploy не применяется —
post-deploy monitor армится только для self-hosting).
- Автоматический approve / авто-rollback self-hosting (ORCH-54 / ORCH-021 политика
ALERT_ONLY) — не меняется.
---
## 5. Инфра-предусловие (вне кода, делает оператор)
Новые Plane-статусы в проекте **ORCH** создаёт оператор через Plane API **ДО** эксплуатации:
`To Analyse`, `Analysis`, `Code-Review`, `Awaiting Deploy`, `Deploying`,
`Monitoring after Deploy` (`Confirm Deploy` уже есть).
Резолвер (`_PLANE_NAME_TO_KEY` + `get_project_states`) подхватывает их **по имени** с
**fail-closed fallback** на `_DEFAULT_STATES` (см. BR-12). Документируется в
`07-infra-requirements.md` (создаёт архитектор) и в `docs/operations/`.
---
## 6. Definition of Done
- Plane показывает осмысленные статусы на каждом этапе.
- Возврат аналитика из Needs Input работает через `To Analyse`.
- Phase A → `Awaiting Deploy`, Phase B → `Deploying`, Phase C → `Monitoring after Deploy`,
окно HEALTHY → `Done`, фейл → `Blocked`.
- `STAGE_TRANSITIONS` не изменён.
- `pytest tests/ -q` — зелёный. Fail-closed покрыт тестами.
- Документация обновлена.

View File

@@ -0,0 +1,178 @@
# 02 — Техническое задание (ТЗ)
**Work Item:** ORCH-066
**Стадия:** analysis → (вход для architecture)
**Автор:** Analyst
> ТЗ фиксирует ТРЕБУЕМОЕ ПОВЕДЕНИЕ и затронутые точки кода. Конкретную архитектуру
> резолвера (точные имена ключей/функций) финализирует архитектор в ADR. Ниже —
> опорный контракт, согласованный с бизнес-запросом Owner.
---
## 1. Задействованные модули `src/`
| Модуль | Роль в задаче |
|--------|---------------|
| `src/plane_sync.py` | **Ядро изменений (слой B):** реестр логических статусов (`_DEFAULT_STATES`), `_PLANE_NAME_TO_KEY`, маппинг стадия→статус (`_STAGE_TO_STATE_KEY`, `STAGE_VISIBILITY_STATE`), хелперы `set_issue_*`. |
| `src/webhooks/plane.py` | Маршрутизация входящего статуса (`handle_issue_updated`): `To Analyse``handle_status_start` (старт **или** resume). |
| `src/stage_engine.py` | Точки ручной простановки статуса: analyst-flow (`Analysis`/`Needs Input`/`In Review`), Phase A (`Awaiting Deploy`), Phase B (`Deploying`), Phase C → `Monitoring after Deploy`, post-deploy monitor → `Done`/`Blocked`. |
| `src/reconciler.py` | F-2 запрос статусов (`To Analyse` в список), Guard 2 skip-list (активные ожидания). |
| `src/stages.py` | **НЕ менять** (инвариант слоя A). Используется только для чтения переходов. |
| `src/config.py` | (При необходимости) kill-switch для новой статусной модели — на усмотрение архитектора (см. §6). |
---
## 2. Изменения статусной модели (слой B)
### 2.1. Реестр логических статусов (`src/plane_sync.py`)
Ввести новые **логические ключи** и их имена в `_PLANE_NAME_TO_KEY`:
| Логический ключ | Plane name | Назначение |
|-----------------|-----------|------------|
| `to_analyse` | `To Analyse` | Вход-триггер (старт + resume аналитика). |
| `analysis` | `Analysis` | Индикация стадии analysis (орк). |
| `code_review` | `Code-Review` | Индикация стадии review (орк). Заменяет `review`. |
| `awaiting_deploy` | `Awaiting Deploy` | Phase A approval-pending (орк). |
| `deploying` | `Deploying` | Phase B прод-деплой идёт (орк). |
| `monitoring` | `Monitoring after Deploy` | Phase C / post-deploy окно (орк). |
Сохранить существующие: `backlog`, `todo`, `in_progress` (backward-compat), `needs_input`,
`in_review`, `blocked`, `done`, `cancelled`, `architecture`, `development`, `testing`,
`approved`, `rejected`. `Cancelled` уже присутствует в `_PLANE_NAME_TO_KEY`.
### 2.2. Fail-closed резолюция (КРИТИЧНО — BR-12)
`get_project_states()` после резолва по API делает `setdefault(k, v)` из `_DEFAULT_STATES`.
Чтобы отсутствие нового статуса в проекте (enduro / Plane down / частичная конфигурация)
**не ломало** конвейер, новые логические ключи в `_DEFAULT_STATES` должны
**алиаситься на существующие UUID** (degrade-to-current):
| Новый ключ | Default-алиас (UUID) | Деградированное поведение |
|------------|----------------------|---------------------------|
| `to_analyse` | = `in_progress` | enduro/старый проект: `In Progress` по-прежнему триггерит старт/resume. |
| `analysis` | = `in_progress` | analysis показывается как `In Progress` (как сейчас). |
| `code_review` | = `review` | review показывается как `Review` (как сейчас). |
| `awaiting_deploy` | = `in_review` | Phase A показывается как `In Review` (как сейчас). |
| `deploying` | = `in_progress` | Phase B показывается как `In Progress` (как сейчас). |
| `monitoring` | = `done` | Phase C показывается как `Done` (как сейчас); монитор затем держит Done / флипает Blocked. |
> Эффект: если оператор НЕ создал новый статус — система работает строго как до ORCH-066
> (никаких падений, никаких 404 от Plane PATCH). Если создал — резолвится по имени и
> используется новый UUID. Это ровно паттерн ORCH-059 AC-7.
### 2.3. Маппинг стадия → статус
`src/plane_sync.py`:
- `_STAGE_TO_STATE_KEY`: `analysis``analysis` (было `in_progress`); `review``code_review`
(было `review`). `deploy` остаётся (управляется Phase A/B/C напрямую, не через
`update_issue_state`). `created`/`architecture`/`development`/`testing`/`done` — без изменений.
- `STAGE_VISIBILITY_STATE`: `review``code_review` (было `review`). Добавить
`analysis``analysis`, если индикация analysis ставится через `set_issue_stage_state`
(решает архитектор; альтернатива — отдельный хелпер `set_issue_analysis`).
- Сохранить совместимость `STAGE_TO_STATE` / `PLANE_STATES` алиасов (импортируются тестами).
### 2.4. Точки простановки статуса
| Место (файл:симв.) | Сейчас | Должно стать |
|--------------------|--------|--------------|
| `webhooks/plane.py` `handle_issue_updated` | `new_state == in_progress``handle_status_start` | `new_state == to_analyse` (с fail-closed: при алиасе совпадает с `in_progress`) → `handle_status_start` |
| `webhooks/plane.py` `start_pipeline` (старт) | статус остаётся `In Progress` | при старте/enqueue analyst орк ставит `Analysis` |
| `webhooks/plane.py` `handle_status_start` (resume из Needs Input) | relaunch на `In Progress`-триггере | relaunch на `To Analyse`-триггере; при relaunch орк ставит `Analysis`. Fork «старт vs resume» (по `get_task_by_plane_id` + `has_active_job_for_task`) — **сохранить как есть.** |
| `stage_engine.py` `_handle_analysis_approved_flow` (artifacts ready) | `set_issue_in_review` | оставить `In Review` (BR-9: In Review только за approve-pending конвейера) ✔ без изменений |
| `stage_engine.py` `_handle_analysis_approved_flow` (questions) | `set_issue_needs_input` | **без изменений** (BR-10) |
| `stage_engine.py` `_handle_self_deploy_phase_a` | `set_issue_in_review` | `Awaiting Deploy` (`set_issue_awaiting_deploy` или аналог) |
| `stage_engine.py` `_handle_self_deploy_phase_b` | (статус не меняет) | `Deploying` |
| `stage_engine.py` advance `deploy → done` (terminal-sync, строка ~338) | `set_issue_done` для всех | **self-hosting:** `Monitoring after Deploy` (перед/вместо арма монитора); **не-self:** `Done` как сейчас |
| `stage_engine.py` `run_post_deploy_monitor` (HEALTHY, окно закрыто) | пишет лог + коммент, статус Plane НЕ трогает (остаётся Done) | `Done` (явно) |
| `stage_engine.py` `run_post_deploy_monitor` (DEGRADED) | пишет лог + alert | `Blocked` |
> **Замечание по terminal-sync (важно для архитектора):** сейчас `advance_stage` на
> `next_stage == "done"` вызывает `set_issue_done` безусловно (строка ~338), затем армит
> post-deploy monitor для self-hosting (~361). Нужно развести: для репо, где
> `post_deploy.post_deploy_applies(repo)` истинно (self-hosting) — ставить `Monitoring
> after Deploy` вместо `Done`, и переложить простановку `Done`/`Blocked` на финал
> монитора (`run_post_deploy_monitor`). Для прочих репо — `Done` как сейчас.
### 2.5. Новые хелперы `src/plane_sync.py`
Добавить тонкие обёртки по образцу `set_issue_in_review` (резолв per-project UUID +
`_set_issue_state_direct`), never-raise при отсутствии issue:
- `set_issue_analysis(work_item_id, project_id=None)`
- `set_issue_code_review(...)` (или через `set_issue_stage_state("review")`)
- `set_issue_awaiting_deploy(...)`
- `set_issue_deploying(...)`
- `set_issue_monitoring(...)`
(Точный набор/именование — на усмотрение архитектора; контракт: per-project резолв +
fail-closed.)
---
## 3. Изменения reconciler (`src/reconciler.py`)
- **F-2** `_reconcile_plane_project`: добавить `to_analyse` в список запрашиваемых
статусов (`list_issues_by_state([... , to_analyse])`) и в `_reconcile_plane_issue`
маршрутизировать `new_state == to_analyse``handle_status_start` (старт при `task is
None`, resume при существующем task без active-job — логика уже в `handle_status_start`).
Сохранить обработку `approved`/`rejected`. При fail-closed алиасе `to_analyse==in_progress`
поведение не дублируется (один и тот же UUID).
- **Guard 2** `_is_blocked_or_needs_input` (F-1 skip): расширить skip-множество активными
ожиданиями — `awaiting_deploy`, `deploying`, `monitoring` — чтобы реконсилер НЕ
«оживлял» их как зависшие (BR-13). Имя метода/семантику можно обобщить
(«human-or-active-wait»), флаг `reconcile_skip_blocked_enabled` продолжает управлять
этим networked-чеком.
> Примечание: F-1 и так не тронет Phase A (`check_deploy_status` red → silent),
> Deploying (active finalizer job), Monitoring (стадия `done`). Guard 2 — явная
> defense-in-depth по требованию Owner.
---
## 4. Изменения API / эндпоинтов
**Нет** новых HTTP-эндпоинтов. `GET /queue` / `GET /status` — без изменений контракта
(статусы Plane там не отражаются). Изменения только во внешней индикации Plane (PATCH
issue state — существующий механизм).
---
## 5. Изменения схемы БД
**Нет.** `tasks` не хранит Plane-статус (источник истины — стадия в БД + Plane API).
Миграции не требуются.
---
## 6. Требования к новым QG checks
**Нет.** `QG_CHECKS` не расширяется. Статусы — индикация, не управление (канон:
машинные вердикты читаются из YAML-frontmatter артефактов, не из Plane-статуса).
Опционально (на усмотрение архитектора): единый kill-switch новой статусной модели
(env-флаг) для безопасного раската, по образцу `staging_infra_tolerance_enabled` /
`reconcile_skip_blocked_enabled`. Не обязателен, т.к. fail-closed алиасинг (§2.2) уже даёт
backward-compatible деградацию.
---
## 7. Артефакты pipeline, создаваемые/обновляемые
- `06-adr/ADR-001-plane-status-model.md` — архитектор (решение по резолверу,
алиасингу, разводке terminal-sync).
- `07-infra-requirements.md` — архитектор (список Plane-статусов для ручного создания
оператором + Plane API инструкция).
- Документация (golden source, тот же PR): `CLAUDE.md` (секция статусной модели),
`docs/architecture/README.md` (секция статусов рядом с ORCH-036/ORCH-021),
`CHANGELOG.md`.
---
## 8. Инварианты (проверяемые)
- `src/stages.py` `STAGE_TRANSITIONS` — байт-в-байт без изменений.
- `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука, merge-gate,
схема БД, `Confirm Deploy`, механизм `Needs Input` — без изменений.
- Все новые `set_issue_*` / резолв — never-raise (Plane down ⇒ degrade, не crash).
- Поведение enduro (не-self) и его терминальный `Done` — без регресса.

View File

@@ -0,0 +1,71 @@
# 03 — Критерии приёмки (Acceptance Criteria)
**Work Item:** ORCH-066
Каждый критерий — чёткое условие PASS/FAIL. Покрытие тестами — см. `04-test-plan.yaml`.
---
## Группа A — Вход и стадия анализа
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-1** | `To Analyse` запускает конвейер | Перевод issue без task в `To Analyse``handle_status_start``start_pipeline` (создаётся task, ветка, enqueue analyst). | Не запускается / запускается на другом статусе. |
| **AC-2** | `To Analyse` делает resume аналитика из Needs Input | Существующий task без active-job + перевод в `To Analyse` → relaunch агента текущей стадии (analyst читает свежие комменты). Fork «старт vs resume» определяется по `get_task_by_plane_id` + `has_active_job_for_task` (как раньше). | Создаётся второй task / двойной запуск / resume не происходит. |
| **AC-3** | Стадия `analysis` индицируется статусом `Analysis` | При старте/relaunch аналитика орк ставит `Analysis`. | Остаётся `In Progress` (при наличии статуса `Analysis` в проекте). |
| **AC-4** | Busy-guard сохранён | `To Analyse` при существующем active-job для task → НЕ relaunch (no double launch). | Двойной запуск агента. |
## Группа B — Code-Review
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-5** | Стадия `review` индицируется `Code-Review` | Вход в стадию `review` → Plane-статус `Code-Review`. | Остаётся `Review` (при наличии нового статуса). |
## Группа C — Self-deploy фазы
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-6** | Phase A → `Awaiting Deploy` | `_handle_self_deploy_phase_a` ставит `Awaiting Deploy` (не `In Review`). | Ставит `In Review` (при наличии нового статуса). |
| **AC-7** | Phase B → `Deploying` | `_handle_self_deploy_phase_b` при успешном `initiate_deploy` ставит `Deploying`. | Статус не меняется / иной. |
| **AC-8** | Phase C → `Monitoring after Deploy` (self) | Финализатор SUCCESS для self-hosting → статус `Monitoring after Deploy`, НЕ `Done` сразу. | Ставит `Done` немедленно (для self-hosting). |
| **AC-9** | Не-self deploy → `Done` без регресса | Для не-self репо (`post_deploy_applies==False`) терминальный `deploy → done` ставит `Done` как сейчас. | Не-self репо получает `Monitoring after Deploy` / иной регресс. |
## Группа D — Post-deploy monitor
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-10** | Чистое окно → `Done` | `run_post_deploy_monitor` HEALTHY + окно исчерпано → статус `Done`. | Остаётся `Monitoring after Deploy` / иной. |
| **AC-11** | Деградация → `Blocked` | `run_post_deploy_monitor` DEGRADED → статус `Blocked` (+ существующий ALERT_ONLY для self). | Остаётся в Monitoring / ставит Done. |
| **AC-12** | Self-hosting монитор не рестартит прод | Тик НИКОГДА не рестартит/откатывает прод-контейнер (ORCH-021 BR-5 сохранён). | Тик трогает прод-контейнер. |
## Группа E — In Review / Needs Input / ветки
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-13** | `In Review` только за approve-pending конвейера | `In Review` ставится лишь для approve артефактов (analyst BRD/ревью), не для Phase A. | Phase A / иные стадии ставят `In Review`. |
| **AC-14** | `Needs Input` без изменений | Поведение `set_issue_needs_input` (analyst `01-questions.md`) идентично прежнему; не расширено на других агентов. | Механизм изменён / расширен. |
| **AC-15** | `Cancelled` — валидный выход из In Review без действий конвейера | Перевод в `Cancelled` → орк не выполняет advance/rollback (индикация, не управление). | Орк совершает действие конвейера на `Cancelled`. |
## Группа F — Fail-closed (КРИТИЧНО)
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-16** | Отсутствие нового статуса не ломает конвейер | Проект без новых статусов (enduro/частичный/Plane down) → `get_project_states` отдаёт default-алиасы; все `set_issue_*`/триггеры работают backward-compatible, без исключений и без 404 PATCH. | Падение / необработанное исключение / зависание задачи. |
| **AC-17** | enduro `In Progress` по-прежнему стартует конвейер | Через `to_analyse`-алиас (= `in_progress` UUID) перевод enduro-issue в `In Progress` запускает старт/resume. | enduro-старт сломан. |
| **AC-18** | Резолв по имени | При наличии статуса в проекте по `name` (`_PLANE_NAME_TO_KEY`) используется его UUID, а не default-алиас. | Используется неверный UUID. |
## Группа G — Reconciler
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-19** | F-2 реконсилирует `To Analyse` | `_reconcile_plane_project` запрашивает `to_analyse` и маршрутизирует к `handle_status_start` (старт/resume при потерянном webhook). | `To Analyse`-старты не реконсилируются. |
| **AC-20** | Guard 2 skip активных ожиданий | Задачи в `Awaiting Deploy` / `Deploying` / `Monitoring after Deploy` НЕ «оживляются» F-1 как зависшие. | Реконсилер advance'ит активное ожидание. |
## Группа H — Инварианты и документация
| ID | Критерий | PASS | FAIL |
|----|----------|------|------|
| **AC-21** | `STAGE_TRANSITIONS` не изменён | `src/stages.py` `STAGE_TRANSITIONS` идентичен (diff пуст). | Любое изменение слоя A. |
| **AC-22** | Реестры/контракты не изменены | `QG_CHECKS`, `check_deploy_status`, exit-коды хука, merge-gate, схема БД, `Confirm Deploy` — без изменений. | Любое изменение перечисленного. |
| **AC-23** | Тесты зелёные | `pytest tests/ -q` проходит полностью; новые fail-closed тесты присутствуют и зелёные. | Любой красный тест. |
| **AC-24** | Документация обновлена (golden source) | `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` обновлены; заведён `06-adr/ADR-001-*`. | Любой из артефактов не обновлён. |

View File

@@ -0,0 +1,184 @@
work_item: ORCH-066
description: >
Тест-план статусной модели Plane (слой B). Покрывает осмысленные статусы этапов,
возврат аналитика через To Analyse, фазы self-deploy, post-deploy monitor,
fail-closed деградацию и reconciler. Слой A (STAGE_TRANSITIONS) проверяется на
неизменность. Все тесты — pytest; Plane API мокается (httpx), как в существующих
tests/test_plane_*.py / tests/test_orch10_states.py.
tests:
# --- Группа A: вход и стадия анализа ---
- id: TC-01
type: unit
description: "To Analyse без существующего task -> handle_status_start -> start_pipeline (старт конвейера)."
module: tests/test_status_trigger.py
covers: [AC-1]
expected: PASS
- id: TC-02
type: integration
description: "To Analyse при существующем task без active-job -> relaunch агента стадии (resume из Needs Input), новый task НЕ создаётся."
module: tests/test_plane_to_analyse_resume.py
covers: [AC-2, BR-11]
expected: PASS
- id: TC-03
type: unit
description: "Старт/relaunch аналитика ставит Plane-статус Analysis (а не In Progress) при наличии статуса в проекте."
module: tests/test_plane_status_model.py
covers: [AC-3]
expected: PASS
- id: TC-04
type: unit
description: "To Analyse при существующем task с active-job -> НЕ relaunch (busy-guard)."
module: tests/test_plane_to_analyse_resume.py
covers: [AC-4]
expected: PASS
# --- Группа B: Code-Review ---
- id: TC-05
type: unit
description: "Вход в стадию review -> Plane-статус Code-Review (маппинг _STAGE_TO_STATE_KEY / STAGE_VISIBILITY_STATE)."
module: tests/test_plane_status_model.py
covers: [AC-5]
expected: PASS
# --- Группа C: self-deploy фазы ---
- id: TC-06
type: unit
description: "_handle_self_deploy_phase_a ставит Awaiting Deploy (не In Review)."
module: tests/test_deploy_approve.py
covers: [AC-6, AC-13]
expected: PASS
- id: TC-07
type: unit
description: "_handle_self_deploy_phase_b при успешном initiate_deploy ставит Deploying."
module: tests/test_deploy_approve.py
covers: [AC-7]
expected: PASS
- id: TC-08
type: integration
description: "Phase C (finalizer SUCCESS) для self-hosting ставит Monitoring after Deploy, НЕ Done; армит post-deploy monitor."
module: tests/test_deploy_terminal_sync.py
covers: [AC-8]
expected: PASS
- id: TC-09
type: integration
description: "Не-self репо: deploy->done ставит Done (без регресса, Monitoring не применяется)."
module: tests/test_deploy_terminal_sync.py
covers: [AC-9]
expected: PASS
# --- Группа D: post-deploy monitor ---
- id: TC-10
type: unit
description: "run_post_deploy_monitor HEALTHY + окно исчерпано -> Plane-статус Done."
module: tests/test_post_deploy.py
covers: [AC-10]
expected: PASS
- id: TC-11
type: unit
description: "run_post_deploy_monitor DEGRADED -> Plane-статус Blocked (+ ALERT_ONLY для self)."
module: tests/test_post_deploy.py
covers: [AC-11]
expected: PASS
- id: TC-12
type: unit
description: "Self-hosting тик НЕ рестартит/не откатывает прод-контейнер (ORCH-021 BR-5 сохранён)."
module: tests/test_post_deploy.py
covers: [AC-12]
expected: PASS
# --- Группа E: In Review / Needs Input / Cancelled ---
- id: TC-13
type: unit
description: "In Review ставится только за approve-pending конвейера (analyst BRD ready), не Phase A."
module: tests/test_analyst_status_only_regression.py
covers: [AC-13]
expected: PASS
- id: TC-14
type: unit
description: "set_issue_needs_input (analyst 01-questions.md) поведение идентично прежнему; не расширено на других агентов."
module: tests/test_plane_status_model.py
covers: [AC-14, BR-10]
expected: PASS
- id: TC-15
type: unit
description: "Перевод в Cancelled -> handle_issue_updated не выполняет advance/rollback (индикация, не управление)."
module: tests/test_plane_webhook.py
covers: [AC-15]
expected: PASS
# --- Группа F: fail-closed (критично) ---
- id: TC-16
type: unit
description: "Проект без новых статусов: get_project_states отдаёт default-алиасы (to_analyse=in_progress, code_review=review, awaiting_deploy=in_review, monitoring=done); исключений нет."
module: tests/test_plane_status_failclosed.py
covers: [AC-16, BR-12]
expected: PASS
- id: TC-17
type: unit
description: "Plane API down -> get_project_states fallback на _DEFAULT_STATES; set_issue_* never-raise."
module: tests/test_plane_status_failclosed.py
covers: [AC-16]
expected: PASS
- id: TC-18
type: integration
description: "enduro In Progress по-прежнему стартует конвейер через to_analyse-алиас."
module: tests/test_plane_status_failclosed.py
covers: [AC-17]
expected: PASS
- id: TC-19
type: unit
description: "Резолв по имени: при наличии статуса в проекте используется его UUID, а не default-алиас."
module: tests/test_orch10_states.py
covers: [AC-18]
expected: PASS
# --- Группа G: reconciler ---
- id: TC-20
type: integration
description: "F-2 _reconcile_plane_project запрашивает to_analyse и маршрутизирует к handle_status_start (потерянный webhook старта/resume)."
module: tests/test_reconciler_plane.py
covers: [AC-19]
expected: PASS
- id: TC-21
type: unit
description: "Guard 2: задачи в Awaiting Deploy / Deploying / Monitoring after Deploy НЕ оживляются F-1 как зависшие."
module: tests/test_reconciler.py
covers: [AC-20, BR-13]
expected: PASS
# --- Группа H: инварианты ---
- id: TC-22
type: unit
description: "STAGE_TRANSITIONS не изменён (явная проверка ключей/значений слоя A)."
module: tests/test_plane_status_model.py
covers: [AC-21]
expected: PASS
- id: TC-23
type: unit
description: "QG_CHECKS реестр и check_deploy_status контракты не изменены."
module: tests/test_plane_status_model.py
covers: [AC-22]
expected: PASS
- id: TC-24
type: integration
description: "Полный прогон pytest tests/ -q зелёный (регрессия)."
module: tests/
covers: [AC-23]
expected: PASS

View File

@@ -0,0 +1,287 @@
# ADR-001: Осмысленная статусная модель Plane (слой B)
**Work Item:** ORCH-066
**Стадия:** architecture
**Автор:** Architect
**Дата:** 2026-06-07
**Статус:** Accepted
> Контракт резолвера, алиасинга и разводки точек простановки статуса. Опирается на
> BRD (`01-brd.md`), ТЗ (`02-trz.md`), критерии приёмки (`03-acceptance-criteria.md`).
> Инфра-предусловие (статусы, создаваемые оператором) — `07-infra-requirements.md`,
> риски — `10-tech-risks.md`.
---
## 1. Контекст
Plane-доска оркестратора семантически перегружена: `In Progress` одновременно
означает «человек запускает конвейер», «идёт анализ», «идёт прод-деплой» и «возврат
из Needs Input». Оператор не различает реальный этап задачи → риск ошибочного ручного
перевода статуса. ORCH-059 уже разгрузил `Approved` отдельным `Confirm Deploy`;
ORCH-066 завершает наведение порядка по утверждённой Owner модели.
**Жёсткое разделение двух слоёв (инвариант проекта):**
| Слой | Что | Источник | ORCH-066 |
|------|-----|----------|----------|
| **A** | `STAGE_TRANSITIONS` — машина стадий | `src/stages.py` | **НЕ трогаем** |
| **B** | Plane-статусы — индикация на доске | `src/plane_sync.py` + точки простановки | **меняем только это** |
Статус — **индикация, не управление**. Машинные вердикты по-прежнему читаются только
из YAML-frontmatter артефактов (канон гейтов). Конвейер движут гейты слоя A; смена
Plane-статуса не может продвинуть/откатить задачу (кроме существующих человеческих
триггеров `To Analyse`/`Approved`/`Rejected`, которые и раньше были входами).
Целевая модель Owner:
```
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
Monitoring after Deploy → Done
```
`[...]` = действие человека (вход-триггер); остальное ставит орк (индикация).
Ветки: **Rejected** (откат), **Needs Input** (только аналитик), **Blocked** (затык/фейл
деплоя/деградация), **Cancelled** (человек отменил задачу).
---
## 2. Решение
### 2.1. Реестр логических статусов (`src/plane_sync.py`)
Вводим 6 новых **логических ключей**. Имена в `_PLANE_NAME_TO_KEY` (резолв по имени из
Plane API):
| Логический ключ | Plane name | Назначение |
|-----------------|-----------|------------|
| `to_analyse` | `To Analyse` | Вход-триггер: старт нового конвейера **и** resume аналитика из Needs Input. |
| `analysis` | `Analysis` | Индикация стадии analysis (орк). |
| `code_review` | `Code-Review` | Индикация стадии review (орк). Заменяет `review` как видимый статус. |
| `awaiting_deploy` | `Awaiting Deploy` | Phase A approval-pending (орк). |
| `deploying` | `Deploying` | Phase B прод-деплой идёт (орк). |
| `monitoring` | `Monitoring after Deploy` | Phase C / post-deploy окно (орк). |
Существующие ключи сохраняются: `backlog`, `todo`, `in_progress`, `needs_input`,
`in_review`, `blocked`, `done`, `cancelled`, `architecture`, `development`, `review`,
`testing`, `approved`, `rejected`. `Cancelled` уже присутствует.
### 2.2. Fail-closed резолюция — **project-relative alias-fallback** (КРИТИЧНО, BR-12)
ТЗ §2.2 предложил статические алиасы на enduro-UUID в `_DEFAULT_STATES`. Архитектурное
уточнение: для **частично сконфигурированного** проекта (оператор создал не все новые
статусы) статический enduro-UUID в orchestrator-проекте даст невалидный `state` → PATCH
422/404. Поэтому деградация делается **относительно того же проекта**, а не на чужой
UUID.
**Два уровня fallback в `get_project_states()` (success-path), строго в порядке:**
1. Резолв по имени из Plane API (как сейчас).
2. **Alias-fallback (новый):** для каждого отсутствующего нового ключа — UUID его
**базового ключа из этого же проекта**:
```python
_STATE_ALIAS_FALLBACK = {
"to_analyse": "in_progress",
"analysis": "in_progress",
"code_review": "review",
"awaiting_deploy": "in_review",
"deploying": "in_progress",
"monitoring": "done",
}
# после резолва по имени, ДО _DEFAULT_STATES.setdefault:
for new_key, base_key in _STATE_ALIAS_FALLBACK.items():
if new_key not in resolved and resolved.get(base_key):
resolved[new_key] = resolved[base_key]
```
3. `_DEFAULT_STATES.setdefault(...)` (как сейчас) — последний резерв для путей, где
API недоступен целиком (`if not project_id: return _DEFAULT_STATES`, полный провал
запроса). В `_DEFAULT_STATES` новые ключи ТОЖЕ добавляются (= enduro-UUID базового
ключа), чтобы любой caller всегда получал полный словарь и `states[key]` не кидал
`KeyError`.
**Эффект деградации:**
| Сценарий | Поведение |
|----------|-----------|
| Orchestrator: все новые статусы созданы | резолв по имени → новые UUID (целевая модель). |
| Orchestrator: создана ЧАСТЬ новых статусов | отсутствующие → **собственный** базовый UUID проекта → индикация деградирует до текущего статуса, PATCH валиден. |
| Enduro (новые статусы не создаются никогда) | alias-fallback → собственные enduro базовые UUID → строго прежнее поведение (`In Progress`/`Review`/`Done`). |
| Plane API down целиком | `_DEFAULT_STATES` (enduro-UUID) — без регресса относительно сегодняшнего поведения. |
Это паттерн ORCH-059 AC-7, усиленный project-relative разрешением. Все `set_issue_*` и
`_set_issue_state_direct` остаются **never-raise** (PATCH-исключение логируется, не
пробрасывается) — индикация деградирует, слой A не затрагивается.
### 2.3. Маппинг стадия → статус
- `_STAGE_TO_STATE_KEY` (живой путь `update_issue_state`→`stage_to_state`):
`analysis` → `analysis` (было `in_progress`); `review` → `code_review` (было `review`).
`deploy` остаётся `in_progress` (управляется Phase A/B/C напрямую). Остальные — без
изменений.
- `STAGE_VISIBILITY_STATE`: `review` → `code_review`; добавить `analysis` → `analysis`
(для консистентности; `set_issue_stage_state` сейчас dormant, но карта обновляется).
- `STAGE_TO_STATE` (legacy/test-only) — обновить `analysis`→`_DEFAULT_STATES["analysis"]`,
`review`→`_DEFAULT_STATES["code_review"]`. UUID-значения **байт-в-байт прежние** (это
алиасы на те же in_progress/review UUID) → тесты на конкретные UUID не краснеют.
### 2.4. Новые хелперы `src/plane_sync.py`
Тонкие обёртки по образцу `set_issue_in_review` (per-project резолв + `_set_issue_state_direct`,
never-raise):
- `set_issue_analysis(work_item_id, project_id=None)`
- `set_issue_code_review(work_item_id, project_id=None)`
- `set_issue_awaiting_deploy(work_item_id, project_id=None)`
- `set_issue_deploying(work_item_id, project_id=None)`
- `set_issue_monitoring(work_item_id, project_id=None)`
`get_project_states` всегда возвращает полный словарь (см. §2.2), поэтому `[key]` не
кидает `KeyError`.
### 2.5. Точки простановки статуса (разводка)
| Файл:место | Сейчас | Должно стать | AC |
|------------|--------|--------------|----|
| `webhooks/plane.py` `handle_issue_updated` | `new_state == in_progress` → `handle_status_start` | `new_state == to_analyse` → `handle_status_start` (при алиасе совпадает с `in_progress`) | AC-1, AC-17 |
| `webhooks/plane.py` `start_pipeline` (успешный старт) | статус остаётся `In Progress` | в конце старта орк ставит `set_issue_analysis` | AC-3 |
| `webhooks/plane.py` `handle_status_start` (resume-ветка) | relaunch агента стадии | при relaunch орк ставит `set_issue_analysis`; fork «старт vs resume» (`get_task_by_plane_id` + `has_active_job_for_task`) — **без изменений** | AC-2, AC-4 |
| `webhooks/plane.py` `_rollback_stage` (reject@analysis, ~583) | `set_issue_in_progress` | `set_issue_analysis` | AC-3 |
| `stage_engine.py` `_handle_analysis_approved_flow` (artifacts ready) | `set_issue_in_review` | **без изменений** (BR-9) | AC-13 |
| `stage_engine.py` `_handle_analysis_approved_flow` (questions) | `set_issue_needs_input` | **без изменений** (BR-10) | AC-14 |
| `stage_engine.py` rollback@analysis (architect conflict, ~669) | `set_issue_in_progress` | `set_issue_analysis` | AC-3 |
| `stage_engine.py` `_handle_self_deploy_phase_a` (~1012) | `set_issue_in_review` | `set_issue_awaiting_deploy` | AC-6, AC-13 |
| `stage_engine.py` `_handle_self_deploy_phase_b` (после `INITIATED` marker) | статус не меняет | `set_issue_deploying` | AC-7 |
| `stage_engine.py` terminal-sync `deploy → done` (~338) | `set_issue_done` для всех | **self (`post_deploy_applies`):** `set_issue_monitoring`; **не-self:** `set_issue_done` как сейчас | AC-8, AC-9 |
| `stage_engine.py` `run_post_deploy_monitor` HEALTHY+окно закрыто (~1260) | статус не трогает | `set_issue_done` (явно) | AC-10 |
| `stage_engine.py` `run_post_deploy_monitor` DEGRADED (~1273) | alert/log | `set_issue_blocked` (+ существующий ALERT_ONLY) | AC-11 |
**Разводка terminal-sync (детально, AC-8/AC-9).** Текущий код безусловно зовёт
`set_issue_done` на `next_stage == "done"`, затем (для self) армит post-deploy monitor.
Разводим по `post_deploy.post_deploy_applies(repo)`:
```python
if next_stage == "done" and work_item_id:
if post_deploy.post_deploy_applies(repo):
set_issue_monitoring(work_item_id) # self: окно наблюдения, НЕ Done сразу
else:
set_issue_done(work_item_id) # не-self: терминальный Done как сейчас
# арм монитора (существующий блок ~361) — без изменений
```
Финальный `Done`/`Blocked` для self-hosting перекладывается на `run_post_deploy_monitor`.
При деградированном алиасе `monitoring==done` self-hosting показывает `Done` и затем
монитор держит `Done`/флипает `Blocked` — поведение идентично сегодняшнему.
**AC-12 (инвариант ORCH-021):** добавление `set_issue_blocked` в DEGRADED-ветку —
**только индикация**; тик по-прежнему НИКОГДА не рестартит/откатывает прод-контейнер
(self-hosting остаётся `ALERT_ONLY`). `set_issue_blocked` — Plane-PATCH, не действие над
контейнером.
**Cancelled (AC-15):** изменений кода НЕ требует. `handle_issue_updated` реагирует только
на `to_analyse`/`approved`/`rejected`; `Cancelled` падает в `else` → «no pipeline action».
Орк не делает advance/rollback — индикация, не управление. Критерий выполнен существующим
кодом.
### 2.6. Reconciler (`src/reconciler.py`)
- **F-2 `_reconcile_plane_project`:** заменить триггер `in_progress` → `to_analyse` в
списке запрашиваемых статусов (`list_issues_by_state([to_analyse, approved, rejected])`)
и в `_reconcile_plane_issue` маршрутизировать `new_state == to_analyse` →
`handle_status_start`. При алиасе `to_analyse == in_progress` (enduro) поведение
идентично текущему (один UUID; `list_issues_by_state` дедуплицирует через `set`). AC-19.
- **Guard 2 `_is_blocked_or_needs_input`:** расширить skip-множество активными ожиданиями
`awaiting_deploy`/`deploying`/`monitoring` (BR-13, AC-20). **Анти-регресс enduro
(КРИТИЧНО):** новые ключи алиасятся на `in_review`/`in_progress`/`done`; добавить их в
skip «как есть» → на enduro `In Progress`/`Done`-задачи начнут ошибочно пропускаться
F-1 (регресс ORCH-053/060). Поэтому активные ожидания включаются в skip **только когда
они РАЗЛИЧНЫ от базовых рабочих статусов проекта** (т.е. реально созданы):
```python
base_working = {states.get(k) for k in (
"backlog","todo","in_progress","in_review","review",
"architecture","development","testing","approved","rejected","done")}
extra_waits = {states.get("awaiting_deploy"),
states.get("deploying"),
states.get("monitoring")} - base_working - {None}
skip_set = {states.get("blocked"), states.get("needs_input")} | extra_waits
return cur in skip_set
```
Enduro (алиасы схлопываются в base) → `extra_waits == {}` → нулевой регресс. Orchestrator
(отдельные UUID) → три реальных статуса в skip → BR-13. Семантику метода обобщаем до
«human-or-active-wait»; флаг `reconcile_skip_blocked_enabled` продолжает гасить этот
networked-чек. F-1 и так структурно не оживляет эти состояния (Phase A: `check_deploy_status`
red → silent; Deploying: active finalizer job → active-job guard; Monitoring: стадия
`done` → не итерируется) — Guard 2 это defense-in-depth по требованию Owner.
### 2.7. Без kill-switch
Отдельный env-флаг новой модели **не вводится**. Раскат естественно гейтится
**инфра-предусловием**: пока оператор не создал новые статусы — alias-fallback (§2.2)
держит строго прежнее поведение; создал — резолв по имени включает новую модель. Это
проще отдельного флага и соответствует принципу «минимум зависимостей». (ТЗ §6 допускает
флаг как опциональный — сознательно отказываемся.)
---
## 3. Затронутые модули (карта изменений)
| Модуль | Изменение |
|--------|-----------|
| `src/plane_sync.py` | `_PLANE_NAME_TO_KEY` +6; `_DEFAULT_STATES` +6 (enduro-alias UUID); `_STATE_ALIAS_FALLBACK` (новое) + применение в `get_project_states`; `_STAGE_TO_STATE_KEY` (analysis/review); `STAGE_VISIBILITY_STATE`; `STAGE_TO_STATE` (legacy); 5 новых `set_issue_*`. |
| `src/webhooks/plane.py` | триггер `in_progress`→`to_analyse` в `handle_issue_updated`; `set_issue_analysis` в `start_pipeline` и resume-ветке `handle_status_start`; `_rollback_stage` reject@analysis → `set_issue_analysis`. |
| `src/stage_engine.py` | Phase A → `set_issue_awaiting_deploy`; Phase B → `set_issue_deploying`; terminal-sync split (`monitoring` vs `done`); post-deploy monitor HEALTHY→`set_issue_done`, DEGRADED→`set_issue_blocked`; rollback@analysis (architect conflict) `set_issue_in_progress`→`set_issue_analysis`. |
| `src/reconciler.py` | F-2 триггер `to_analyse`; Guard 2 skip-set + анти-регресс subtraction. |
| `src/stages.py` | **НЕ трогаем** (инвариант слоя A). |
| `src/config.py` | Без изменений (kill-switch не вводится). |
---
## 4. Инварианты (проверяемые, AC-21/AC-22)
- `src/stages.py` `STAGE_TRANSITIONS` — diff пуст (байт-в-байт).
- `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука (0/1/2),
merge-gate, `check_branch_mergeable`/`check_staging_image_fresh`, схема БД — без изменений.
- `Confirm Deploy` (ORCH-059), механизм `Needs Input` (analyst-only) — без изменений.
- Новых HTTP-эндпоинтов нет; `GET /queue`/`GET /status` контракт без изменений.
- Миграций БД нет (`tasks` не хранит Plane-статус; источник истины — стадия в БД + Plane API).
- Все новые `set_issue_*` / резолв — never-raise.
- Не-self (enduro) терминальный `deploy → Done` — без регресса.
---
## 5. Последствия
**Плюсы**
- Доска читаема: каждый этап = осмысленный статус; человеческие входы визуально отделены
от индикации.
- `In Progress` разгружен: больше не «всё подряд».
- Fail-closed усилен (project-relative): частичная конфигурация не ломает ни индикацию,
ни конвейер.
- Слой A нетронут → нулевой риск для машины стадий и гейтов всех проектов (self-hosting).
- Нет нового флага/таблицы → меньше движущихся частей.
**Минусы / ограничения**
- Требуется ручное инфра-действие оператора (создать 6 статусов в проекте ORCH) — до
этого orchestrator деградирует до старой индикации (см. `07-infra-requirements.md`).
- Статусы кэшируются per-process (`_STATES_CACHE`): после создания статусов нужен
`reload_project_states()` или рестарт **staging** (не прод — см. self-hosting риск).
- Guard-2 subtraction добавляет немного логики; покрывается тестами (enduro-алиас → пустой
extra; orchestrator → три статуса).
**Self-hosting (⚠️):** изменения — слой B (Plane-индикация) + reconciler-гварды; машина
стадий и контракты деплоя нетронуты. Выкладка ОБЯЗАТЕЛЬНО через `deploy-staging` (8501)
до прод-деплоя орка. Прод-контейнер не рестартить в рамках задачи вне штатного staging-гейта.
---
## 6. Альтернативы (отклонены)
- **Статический enduro-UUID алиас (ТЗ §2.2 буквально):** ломается на частичной
конфигурации orchestrator-проекта (чужой UUID → PATCH 422). Заменён project-relative
alias-fallback (§2.2).
- **Глобальный env kill-switch новой модели:** избыточен — инфра-предусловие уже даёт
естественный гейт раската (§2.7).
- **Хранить Plane-статус в `tasks` (миграция БД):** не нужно; источник истины — стадия +
живой Plane API. Нарушило бы инвариант «без лишних зависимостей».
- **Менять `STAGE_TRANSITIONS` ради новых статусов:** запрещено (инвариант слоя A);
статусы — индикация, отделены от машины стадий.

View File

@@ -0,0 +1,96 @@
# 07 — Требования к инфраструктуре
**Work Item:** ORCH-066
**Автор:** Architect
**Дата:** 2026-06-07
> ORCH-066 не меняет топологию (контейнеры/порты/сеть — без изменений, см.
> `docs/operations/INFRA.md`). Единственное инфра-действие — создание новых
> Plane-статусов в проекте **ORCH** руками оператора через Plane API. Это
> **предусловие эксплуатации**, не часть кодового PR.
---
## 1. Что нужно сделать оператору (ДО эксплуатации новой модели)
Создать в Plane-проекте **ORCH** следующие статусы (states) с точными именами —
резолвер сопоставляет их по `name` (`_PLANE_NAME_TO_KEY`):
| Plane name (точно) | Логический ключ | Группа Plane (рекомендуемая) | Назначение |
|--------------------|-----------------|------------------------------|------------|
| `To Analyse` | `to_analyse` | unstarted / started | Человеческий вход: старт конвейера + resume аналитика из Needs Input. |
| `Analysis` | `analysis` | started | Индикация стадии анализа. |
| `Code-Review` | `code_review` | started | Индикация стадии review. |
| `Awaiting Deploy` | `awaiting_deploy` | started | Phase A: ожидание ручного approve на прод-деплой. |
| `Deploying` | `deploying` | started | Phase B: идёт прод-деплой. |
| `Monitoring after Deploy` | `monitoring` | started | Phase C / окно пост-деплой наблюдения. |
`Confirm Deploy` (ORCH-059) и базовые статусы (`Backlog`, `Todo`, `In Progress`,
`Architecture`, `Development`, `Review`, `Testing`, `Approved`, `Rejected`, `Done`,
`Cancelled`, `Needs Input`, `In Review`, `Blocked`) уже существуют — **не трогать**.
> ⚠️ **Точность имён критична.** Резолв идёт по строковому `name`. Опечатка/иной регистр
> → статус не сопоставится → ключ деградирует на собственный базовый UUID проекта
> (alias-fallback, ADR §2.2): индикация откатится к старому статусу, но конвейер
> продолжит работать. Дефис в `Code-Review` — обязателен.
---
## 2. Plane API — как создать статус
Эндпоинт (как в `src/plane_sync.py`, `PLANE_BASE = {plane_api_url}/api/v1`):
```
POST {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
Headers: X-API-Key: <PLANE_API_TOKEN> (или соответствующий бот-токен с правами)
Body (JSON):
{ "name": "To Analyse", "group": "started", "color": "#3f76ff" }
```
Повторить для каждого имени из таблицы §1. `group` влияет только на колонку доски;
оркестратор `group` не читает (резолв строго по `name`). `color` — на вкус оператора.
Проверка после создания:
```
GET {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
```
В ответе должны присутствовать все 6 имён.
---
## 3. Сброс кэша статусов (важно)
`get_project_states` кэширует резолв per-process (`_STATES_CACHE`). После создания
статусов оркестратор подхватит их **только** после сброса кэша:
- штатно — `plane_sync.reload_project_states(project_id)` (или рестарт процесса);
- на **staging** (8501) — безопасный рестарт песочницы;
- на **прод** (8500) — **НЕ рестартить контейнер ради этого** в рамках задачи
(self-hosting: общий контейнер всех проектов). Кэш заполняется при первом обращении к
проекту; если статусы созданы ДО первого PATCH в цикле новой версии — отдельный сброс не
нужен. Если созданы позже — дождаться штатного цикла обновления/деплоя орка.
---
## 4. Порядок раската (рекомендация)
1. Слить кодовый PR ORCH-066 через `deploy-staging` (8501).
2. Создать 6 статусов в проекте ORCH (§1§2).
3. Сбросить кэш / поднять staging, прогнать sandbox-задачу — убедиться, что доска
показывает `Analysis` / `Code-Review` / `Awaiting Deploy` / `Deploying` /
`Monitoring after Deploy` / `Done` на соответствующих этапах.
4. Прод-деплой орка штатным self-deploy (Phase A → approve → Phase B/C).
**До шага 2** система работает строго как до ORCH-066 (alias-fallback) — раскат
безопасно обратим: не создавать/удалить статусы = откат индикации к старой модели,
без изменения кода.
---
## 5. Что НЕ требуется
- Никаких изменений docker-compose, портов, сети, томов, `.env`/`.env.staging`.
- Никаких миграций БД (`tasks` не хранит Plane-статус).
- Никаких изменений в проекте **enduro-trails** — там новые статусы не создаются;
alias-fallback держит прежнюю индикацию (`In Progress`/`Review`/`Done`).

View File

@@ -0,0 +1,31 @@
# 10 — Технические риски
**Work Item:** ORCH-066
**Автор:** Architect
**Дата:** 2026-06-07
Риски слоя B (Plane-индикация). Слой A (`STAGE_TRANSITIONS`/гейты) не затрагивается, поэтому
класс «сломали конвейер» структурно исключён — худший исход любого риска ниже = неверная
**индикация**, не остановка конвейера.
| ID | Риск | Вероятность | Влияние | Митигация |
|----|------|-------------|---------|-----------|
| **R1** | Частичная конфигурация: оператор создал не все 6 статусов в ORCH → отсутствующий ключ деградирует. Наивный статический enduro-UUID дал бы невалидный `state` (PATCH 422) на orchestrator-issue. | Средняя | Средн. | **Project-relative alias-fallback** (ADR §2.2): отсутствующий ключ → собственный базовый UUID проекта → PATCH валиден, индикация откатывается к текущему статусу. Покрыть тестом partial-config. |
| **R2** | Enduro-регресс через Guard 2: новые ключи алиасятся на `in_progress`/`in_review`/`done`; наивное добавление в skip-set заставит F-1 пропускать enduro `In Progress`/`Done` → сломанная реконсиляция (ORCH-053/060). | Средняя | Высок. | **Subtraction базовых рабочих статусов** (ADR §2.6): `extra_waits -= base_working`. На enduro (алиасы схлопнуты) `extra_waits == {}` → нулевой регресс. Тест: enduro-алиас не добавляет skip, orchestrator-distinct добавляет. |
| **R3** | Двойной триггер старта: F-2 reconciler и webhook оба маршрутизируют `to_analyse`; при алиасе `to_analyse == in_progress` возможен повтор. | Низкая | Низк. | `list_issues_by_state` дедуплицирует UUID через `set`; active-job guard + atomic create-claim в `handle_status_start` (`get_task_by_plane_id` + `has_active_job_for_task`) — без двойного старта (AC-4). Сохранить fork как есть. |
| **R4** | Кэш статусов: после создания статусов `_STATES_CACHE` отдаёт старый резолв до сброса → доска не обновляется. | Средняя | Низк. | `reload_project_states()` / рестарт **staging**. Документировано в `07-infra-requirements.md §3`. Прод-рестарт ради кэша — запрещён (self-hosting). |
| **R5** | Опечатка в имени статуса оператором (`Code Review` без дефиса и т.п.) → ключ не резолвится. | Средняя | Низк. | Резолв по точному `name`; при промахе — alias-fallback (деградация, не падение). Точные имена и проверка в `07-infra-requirements.md §12`. |
| **R6** | Terminal-sync split: ошибка ветвления `post_deploy_applies` → enduro получает `Monitoring after Deploy` вместо `Done` (регресс AC-9) или self уходит в `Done` минуя окно (AC-8). | Низкая | Средн. | Единый источник условности — `post_deploy.post_deploy_applies(repo)` (та же функция, что армит монитор). Тесты AC-8 (self→monitoring) и AC-9 (не-self→done). |
| **R7** | Phase B: `set_issue_deploying` поставлен до фактического старта детача → ложная индикация при провале `initiate_deploy`. | Низкая | Низк. | Ставить `set_issue_deploying` **после** успешного `initiate_deploy` и записи `INITIATED` marker (ADR §2.5); провал `initiate_deploy` оставляет `Awaiting Deploy` + просьбу повторить approve. |
| **R8** | Post-deploy DEGRADED → `set_issue_blocked` ошибочно трактуется как «действие над продом». | Низкая | Высок.(если) | `set_issue_blocked` — только Plane-PATCH. Тик остаётся `ALERT_ONLY`, НИКОГДА не рестартит/откатывает прод-контейнер (AC-12, ORCH-021 BR-5). Явный тест: self DEGRADED не трогает контейнер. |
| **R9** | Plane API недоступен в момент простановки статуса → PATCH падает. | Низкая | Низк. | Все `set_issue_*`/`_set_issue_state_direct` — never-raise (логируют, не пробрасывают). Индикация пропускается, слой A не затронут. |
| **R10** | Регресс на тестах, читающих `STAGE_TO_STATE`/`PLANE_STATES` конкретные UUID. | Низкая | Низк. | Новые ключи в `_DEFAULT_STATES` = алиасы на те же in_progress/review/done UUID → значения байт-в-байт; `STAGE_TO_STATE` analysis/review остаются прежними UUID (ADR §2.3). |
| **R11** | Self-hosting: выкладка орка минуя staging. | Низкая | Высок. | Обязательный `deploy-staging` гейт (8501); прод не рестартить вне штатного self-deploy. Раскат обратим (не создавать статусы = старое поведение). |
## Сводный вывод
Все риски снижаемы в рамках принятой архитектуры; ни один не способен остановить конвейер
(слой A инвариантен). Два ключевых требуют аккуратной реализации и обязательных тестов:
**R1** (project-relative alias-fallback) и **R2** (Guard-2 anti-regress subtraction) —
оба зафиксированы в ADR §2.2 и §2.6 как явные контракты. Эскалации `arch:major-change` не
требуется: изменение локализовано в слое B, без новых компонентов/стадий/QG/миграций.

View File

@@ -0,0 +1,89 @@
---
type: review
work_item_id: ORCH-066
verdict: APPROVED
version: 1
---
# Review ORCH-066
## Summary
Осмысленная статусная модель Plane (слой B — индикация). Реализация затрагивает
строго слой B (`src/plane_sync.py`, точки простановки в `src/stage_engine.py` /
`src/webhooks/plane.py` / `src/reconciler.py`) и **не трогает слой A**
(`src/stages.py::STAGE_TRANSITIONS` — diff пуст). Все 4 оси проверки (ТЗ, ADR,
качество кода, тесты) и проверка документации — пройдены. `pytest tests/ -q`:
**774 passed**. Вердикт — **APPROVED**.
## Соответствие ТЗ (02-trz.md)
- §2.1 — 6 новых логических ключей в `_PLANE_NAME_TO_KEY` + `_DEFAULT_STATES`. ✔
- §2.2 — fail-closed резолюция (BR-12). ✔ (реализована усиленная project-relative
версия — см. ADR ниже).
- §2.3 — `_STAGE_TO_STATE_KEY` (analysis→analysis, review→code_review),
`STAGE_VISIBILITY_STATE`, legacy `STAGE_TO_STATE` (UUID байт-в-байт прежние). ✔
- §2.4 — точки простановки разведены (handle_issue_updated триггер `to_analyse`,
start_pipeline/resume → Analysis, Phase A → Awaiting Deploy, Phase B → Deploying,
terminal-sync split, post-deploy HEALTHY→Done / DEGRADED→Blocked,
rollback@analysis → Analysis). ✔
- §2.5 — 5 новых never-raise хелперов `set_issue_*`. ✔
- §3 — reconciler F-2 триггер `to_analyse` (+ resume-ветка), Guard 2 skip-set с
вычитанием base_working. ✔
- §4/§5/§6 — нет новых эндпоинтов, нет миграций БД, `QG_CHECKS` не расширен. ✔
## Соответствие ADR (06-adr/ADR-001)
- §2.2 project-relative alias-fallback (`_STATE_ALIAS_FALLBACK`, применён ДО
`_DEFAULT_STATES.setdefault`) — реализован точно по контракту, деградация на
собственный базовый UUID проекта, PATCH остаётся валидным на частичной
конфигурации. ✔
- §2.5 terminal-sync split по `post_deploy.post_deploy_applies(repo)` — реализован
как в ADR (self → Monitoring, не-self → Done). ✔
- §2.6 Guard 2 анти-регресс (extra_waits base_working {None}) — реализован
дословно, enduro-алиасы схлопываются → нулевой регресс. ✔
- §2.7 без kill-switch — config.py не изменён (diff пуст). ✔
## Качество кода
- Все новые `set_issue_*` следуют образцу `set_issue_in_review` (per-project резолв
+ `_set_issue_state_direct`), контракт never-raise сохранён, есть docstrings. ✔
- Post-deploy/terminal-sync простановки обёрнуты в try/except с warning-логом
(never break the tick). ✔
- Переменные в scope корректны (`work_item_id` определён до всех новых вызовов в
`start_pipeline`/`handle_status_start`/stage_engine). ✔
- AC-12 соблюдён: `set_issue_blocked` в DEGRADED-ветке — только индикация, тик
прод-контейнер не трогает. ✔
## Качество тестов
- Содержательные, не тривиальные: `test_plane_status_failclosed.py`
(TC-16/17/18 — partial project, API down, never-raise сеттеров, enduro alias
старт), `test_plane_to_analyse_resume.py`, `test_plane_status_model.py`,
`test_deploy_terminal_sync.py` (self/не-self split), `test_post_deploy_integration.py`,
`test_reconciler*.py` (F-2 to_analyse + Guard 2). ✔
## Инварианты (AC-21/AC-22)
- `src/stages.py` — diff 0 строк (STAGE_TRANSITIONS байт-в-байт). ✔
- `src/qg/checks.py` — diff 0 строк (QG_CHECKS, check_deploy_status). ✔
- `src/config.py` — diff 0 строк. ✔
- Схема БД — без миграций. ✔
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
## Документация
Обновлена в том же PR (golden source соблюдён):
- `CLAUDE.md` — добавлена секция «Статусная модель Plane (ORCH-066)». ✔
- `docs/architecture/README.md` — секция «Осмысленная статусная модель Plane
(ORCH-066)» + обновлён статусный footer. ✔
- `CHANGELOG.md` — подробная запись в [Unreleased]/Added. ✔
- `06-adr/ADR-001-plane-status-model.md` — заведён. ✔
- `07-infra-requirements.md` — присутствует (инфра-предусловие: 6 Plane-статусов
создаёт оператор). ✔
Изменения `src/` полностью отражены в документации → требование
«документация обновлена при изменении src/» выполнено.

View File

@@ -0,0 +1,77 @@
---
type: test-report
work_item_id: ORCH-066
result: PASS
---
# Test Report — ORCH-066
Осмысленная статусная модель Plane (слой B — индикация). Прогон полного регресса +
покрытие тест-плана `04-test-plan.yaml` + проверка инвариантов слоя A.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: feature/ORCH-066-plane
- Дата: 2026-06-07
## Результаты по тест-плану (04-test-plan.yaml)
| TC ID | Покрывает | Описание | Модуль | Результат |
|-------|-----------|----------|--------|-----------|
| TC-01 | AC-1 | To Analyse без task → start_pipeline | test_status_trigger.py | PASS |
| TC-02 | AC-2,BR-11 | To Analyse resume аналитика, без двойного task | test_plane_to_analyse_resume.py | PASS |
| TC-03 | AC-3 | Старт/relaunch → статус Analysis | test_plane_status_model.py | PASS |
| TC-04 | AC-4 | Busy-guard: active-job → не relaunch | test_plane_to_analyse_resume.py | PASS |
| TC-05 | AC-5 | review → статус Code-Review | test_plane_status_model.py | PASS |
| TC-06 | AC-6,AC-13 | Phase A → Awaiting Deploy (не In Review) | test_deploy_approve.py | PASS |
| TC-07 | AC-7 | Phase B → Deploying | test_deploy_approve.py | PASS |
| TC-08 | AC-8 | Phase C self → Monitoring after Deploy | test_deploy_terminal_sync.py | PASS |
| TC-09 | AC-9 | Не-self deploy→done → Done (без регресса) | test_deploy_terminal_sync.py | PASS |
| TC-10 | AC-10 | Post-deploy HEALTHY → Done | test_post_deploy.py | PASS |
| TC-11 | AC-11 | Post-deploy DEGRADED → Blocked | test_post_deploy.py | PASS |
| TC-12 | AC-12 | Self-тик не рестартит прод | test_post_deploy.py | PASS |
| TC-13 | AC-13 | In Review только за approve-pending | test_analyst_status_only_regression.py | PASS |
| TC-14 | AC-14,BR-10 | Needs Input без изменений | test_plane_status_model.py | PASS |
| TC-15 | AC-15 | Cancelled → нет действий конвейера | test_plane_webhook.py | PASS |
| TC-16 | AC-16,BR-12 | Fail-closed default-алиасы, нет исключений | test_plane_status_failclosed.py | PASS |
| TC-17 | AC-16 | Plane API down → fallback, never-raise | test_plane_status_failclosed.py | PASS |
| TC-18 | AC-17 | enduro In Progress стартует через алиас | test_plane_status_failclosed.py | PASS |
| TC-19 | AC-18 | Резолв по имени → корректный UUID | test_orch10_states.py | PASS |
| TC-20 | AC-19 | F-2 реконсилирует To Analyse | test_reconciler_plane.py | PASS |
| TC-21 | AC-20,BR-13 | Guard 2 skip активных ожиданий | test_reconciler.py | PASS |
| TC-22 | AC-21 | STAGE_TRANSITIONS не изменён | test_plane_status_model.py | PASS |
| TC-23 | AC-22 | QG_CHECKS/check_deploy_status не изменены | test_plane_status_model.py | PASS |
| TC-24 | AC-23 | Полный регресс pytest зелёный | tests/ | PASS |
Все 24 тест-кейса — PASS.
## Инварианты слоя A (AC-21 / AC-22)
Diff против `origin/main` (merge-base `4815e378`):
- `src/stages.py` (STAGE_TRANSITIONS) — diff пуст ✔
- `src/qg/checks.py` (QG_CHECKS, check_deploy_status) — diff пуст ✔
- `src/config.py` (без kill-switch) — diff пуст ✔
## Smoke test API (TestClient — прод-контейнер 8500 не трогался)
> `curl` в окружении недоступен; smoke прогнан через FastAPI TestClient (lifespan),
> без рестарта/обращения к прод-контейнеру (self-hosting safety).
| Endpoint | Статус | Тело (фрагмент) |
|----------|--------|-----------------|
| GET /health | 200 | `{"status":"ok","service":"orchestrator"}` |
| GET /status | 200 | `{"active_tasks":[...]}` |
| GET /queue | 200 | `{"counts":{...},"max_concurrency":1,...}` |
## Вывод pytest
```
======================= 774 passed, 1 warning in 17.68s ========================
```
(единственный warning — PydanticDeprecatedSince20 в src/config.py, предсуществующий,
не связан с ORCH-066)
Прогон по модулям тест-плана: `117 passed` (ORCH-066-специфичные файлы).
## Итог
PASS — все тесты зелёные (774 passed), все 24 TC покрыты, инварианты слоя A
сохранены (diff пуст), smoke-эндпоинты отвечают 200. Review-вердикт APPROVED.
Задача готова к переходу на стадию deploy-staging.

View File

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

View File

@@ -0,0 +1,39 @@
---
staging_status: SUCCESS
timestamp: 2026-06-07T22:01:57Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (port 8501),
run canonically via `docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`.
**Result: 8/10 checks PASS — exit code 0 (advance).**
All REAL (pipeline) checks are green. The two failing checks are the known
SANDBOX_INFRA-only checks C9a/C9b (sandbox branch / analyst-job — depend on
SANDBOX bot accounts being project members, not on the pipeline), which are
waived under ORCH-061 since every REAL check passed.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
## Check breakdown
| Block | Check | Result |
|-------|-------|--------|
| A SMOKE | A1 GET /health → 200 status=ok | PASS |
| A SMOKE | A2 GET /queue → 200 with counts/max_concurrency/resilience | PASS |
| A SMOKE | A3 ORCH_STAGING=true (not prod) | PASS |
| B ACCESS | B4 Plane: sandbox project accessible | PASS |
| B ACCESS | B5 Gitea: orchestrator-sandbox accessible, push=true | PASS |
| B ACCESS | B6 Registry: sandbox present, prod ET/ORCH absent | PASS |
| C E2E | C7 Create issue in Plane SANDBOX | PASS |
| C E2E | C8 Trigger pipeline via /webhook/plane | PASS |
| C E2E | C9a Branch appears in orchestrator-sandbox | FAIL (waived — sandbox-infra) |
| C E2E | C9b Analyst job enqueued in staging queue | FAIL (waived — sandbox-infra) |
CLEANUP completed: test Plane issue deleted (HTTP 204); no branch to delete.

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-066
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.

View File

@@ -0,0 +1,7 @@
# Business Request: [высокий] Telegram tracker: bump + статусы Plane + кликабельный номер задачи
Work Item ID: ORCH-067
## Description
TBD

View File

@@ -0,0 +1,158 @@
# BRD — ORCH-067: Telegram tracker (bump + статусы Plane + кликабельный номер задачи)
Work Item: **ORCH-067**
Тип: **Багфикс + enhancement**
Приоритет: высокий
Компонент: Telegram live-tracker и уведомления оркестратора (`src/notifications.py`)
Расширяет: открытый баг seq=55 («bump не сработал, регресс ORCH-042»)
---
## 1. Бизнес-контекст и проблема
Оркестратор ведёт по одной «живой карточке» (live-tracker) на каждую задачу в Telegram
(`src/notifications.py`). Карточка тихо обновляется на каждом переходе стадии, а отдельными
пингами шлются только события, требующие внимания владельца (approve-gate, деплой-фейл,
падение агента, ошибка задачи).
Сейчас есть четыре боли:
1. **bump не работает в проде.** Диагностика оператора: код режима `bump` в
`update_task_tracker` корректен (delete старого → sendMessage вниз → repoint
`tracker_message_id`), НО в проде `tracker_mode="edit"` (дефолт `src/config.py:408`),
а `ORCH_TRACKER_MODE=bump` не выставлен. Карточка обновляется edit-in-place и остаётся
«вверху» ленты, тонет под новыми сообщениями — наблюдатель не видит актуального
состояния без скролла.
2. **Карточка показывает внутренние названия стадий, а не Plane-статусы.** После ввода
осмысленной статусной модели Plane (ORCH-066) карточка по-прежнему рендерит внутренние
ярлыки стадий (Анализ/Архитектура/…), а текущий статус задачи в терминах, понятных
наблюдателю в Plane (To Analyse → Analysis → In Review → … → Done), в шапке карточки
не отражён. Особенно теряется состояние **ожидания согласования BRD** = Plane-статус
`In Review`: сейчас это лишь строка «✅/⏸️ Подтверждение BRD … ⏳», не выраженная как
полноценный статус.
3. **Номер задачи в карточке некликабелен.** `ORCH-066` в карточке — обычный текст;
чтобы открыть задачу в Plane, наблюдателю приходится искать её вручную.
4. **Номер задачи некликабелен и во всех остальных уведомлениях орка** (approve-requested,
QG-fail, deploy SUCCESS/FAIL, Needs Input, прод-деплой и т. п.) — везде, где упоминается
`work_item_id`, это просто текст.
## 2. Цель
Сделать live-tracker и уведомления орка наблюдаемыми «из коробки»:
- bump работает по умолчанию (карточка падает вниз свежим сообщением при каждом обновлении,
ровно одна карточка на задачу, без спама и дублей);
- карточка явно показывает текущий Plane-статус по модели ORCH-066, включая человеческие
гейты (`⏸️ In Review` — согласование BRD, `⏸️ Awaiting Deploy` — ожидание Confirm Deploy,
`❓ Needs Input` — нужны уточнения);
- номер задачи кликабелен в карточке и во всех Telegram-уведомлениях орка и ведёт на
страницу задачи в Plane.
## 3. Заинтересованные стороны
- **Owner (Слава)** — основной потребитель карточки и уведомлений; источник 4 требований.
- **Агенты конвейера** — косвенно (карточка отражает их прогресс; поведение агентов не меняется).
- **Другие проекты (enduro-trails)** — общий инстанс/БД; изменения не должны вызывать регресс.
## 4. Объём работ (scope)
### 4.1. Требование 1 — bump по умолчанию
- Режим `bump` должен быть поведением по умолчанию: при каждом обновлении карточка
удаляется и пересоздаётся внизу ленты, одна карточка на задачу, тихо
(`disable_notification`), без дублей.
- Инвариант «одна карточка на задачу» сохраняется в обоих режимах (`edit` остаётся как
опция через env).
- Транзиентный фейл `send` не должен обнулять `tracker_message_id` и плодить дубли
(инвариант уже заложен в коде — сохранить).
### 4.2. Требование 2 — статусы карточки как в Plane (модель ORCH-066)
- В шапке/верхней части карточки явно отображается **текущий Plane-статус** задачи по
модели ORCH-066.
- Полный маппинг состояний (имена — финальные из модели ORCH-066):
```
To Analyse → Analysis → In Review (⏸️ ожидание согласования BRD) → Architecture →
Development → Code-Review → Testing → Awaiting Deploy (⏸️ ожидание Confirm Deploy) →
Deploying → Monitoring after Deploy → Done
```
Ветки: `Needs Input` (аналитик задал вопросы), `Blocked`, `Rejected`, `Cancelled`.
- Человеческие гейты отражаются как ПОЛНОЦЕННЫЕ статусы с паузой:
- согласование BRD → «⏸️ In Review — ожидание согласования BRD»;
- ожидание прод-деплоя → «⏸️ Awaiting Deploy — ожидание Confirm Deploy»;
- вопросы аналитика → «❓ Needs Input — нужны уточнения».
- Существующая семантика строки «Подтверждение BRD» сохраняется (время ожидания/«твоё
время»), но статус карточки при этом явно показывает In Review (approve-pending).
### 4.3. Требование 3 — кликабельный номер задачи в карточке
- `work_item_id` (напр. `ORCH-066`) в карточке — гиперссылка на страницу задачи Plane:
`https://<PLANE_WEB_BASE>/<workspace_slug>/projects/<project_id>/issues/<issue_id>/`.
- Источники частей URL:
- `PLANE_WEB_BASE` — из конфигурации (env, поле `plane_web_url` / `ORCH_PLANE_WEB_URL`;
значение прод — `plane.mva154.duckdns.org`); fail-safe: не задан → номер без ссылки;
- `workspace_slug` — `plane_workspace_slug` (уже есть в settings, прод — `ag_proj`);
- `project_id` — резолвится per-task по репозиторию задачи (ORCH / Sandbox);
- `issue_id` (UUID) — из БД: колонка `tasks.plane_issue_id`.
- Рендер через `<a href="...">ORCH-NNN</a>` (`parse_mode=HTML` уже включён);
HTML в title/тексте экранируется, чтобы не сломать разметку.
### 4.4. Требование 4 — кликабельный номер во ВСЕХ уведомлениях орка
- Единый хелпер (напр. `plane_issue_link(work_item_id, plane_issue_id, project_id) -> html`)
строит кликабельный номер с fail-safe; применяется во всех точках `send_telegram`/
`notify_*`, где упоминается `work_item_id` (approve-requested, QG-fail, deploy
SUCCESS/FAIL, Needs Input, прод-деплой, alert'ы launcher/merge_gate/job_reaper/
security_gate/reconciler/main).
## 5. Вне объёма (out of scope)
- Транспорт `send_telegram` / `edit_telegram` / `delete_telegram` (parse_mode HTML уже есть) — не трогать.
- Инвариант «одна карточка на задачу» — не нарушать (не плодить дубли).
- Логика `disable_notification` (карточка тихая; пингуют только alert-хелперы) — не трогать.
- `STAGE_TRANSITIONS`, Quality Gates, схема БД — НЕ менять.
- Изменение поведения агентов/конвейера.
## 6. Зависимости
- Маппинг статусов (требование 2) опирается на статусную модель ORCH-066. ORCH-066 уже в
конвейере на стадии deploy. Эту задачу делать ПОСЛЕ прода ORCH-066, чтобы имена статусов
совпали. Если ORCH-066 ещё не в проде на момент разработки — использовать согласованные
финальные имена из модели: To Analyse, Analysis, Code-Review, Awaiting Deploy, Deploying,
Monitoring after Deploy, In Review, Needs Input, Blocked, Cancelled, Done.
- Конфигурация `plane_web_url` / `plane_workspace_slug` уже существует в `src/config.py`
(ORCH-017); реестр проектов `src/projects.py` (`get_project_by_repo().plane_project_id`)
уже даёт per-task project_id.
## 7. Fail-safe (обязательно)
- Нет `PLANE_WEB_BASE` / нет `plane_issue_id` / нет `project_id` / loopback-база →
показывать номер БЕЗ ссылки, **не падать**.
- HTML-экранирование пользовательского текста (title и пр.) во всех сообщениях с
`parse_mode=HTML`.
- Bump: транзиентный фейл `send` не обнуляет `tracker_message_id` и не плодит дубли.
- Любая ошибка построения статуса/ссылки никогда не должна ронять рендер карточки или
отправку уведомления (degrade gracefully).
## 8. Критерии успеха (Definition of Done)
- Bump работает из коробки: карточка падает вниз при обновлении, одна на задачу.
- Карточка показывает Plane-статус новой модели, включая `⏸️ In Review` (согласование BRD),
`⏸️ Awaiting Deploy`, `❓ Needs Input`.
- Номер задачи кликабелен в карточке И во всех уведомлениях орка (ведёт на страницу Plane).
- Fail-safe покрыт тестами (нет URL/plane_id/project → без ссылки, не падает;
HTML-экранирование).
- `pytest tests/ -q` зелёный.
- Документация обновлена в том же PR: `CLAUDE.md` (раздел нотификаций/tracker),
`CHANGELOG.md`, ADR per-work-item.
## 9. Риски
- **Регресс enduro-trails.** Смена дефолта `tracker_mode` на bump меняет поведение для всех
проектов. Митигация: bump уже реализован и протестирован концептуально; инвариант «одна
карточка» сохранён; env-переключатель `edit` остаётся.
- **Поломка HTML-разметки** при неэкранированном title → сообщение не доставится. Митигация:
обязательное `html.escape` + тесты.
- **Источник «истинного» Plane-статуса** для веток, не выводимых из `tasks.stage`
(Needs Input/Blocked/Rejected/Cancelled, Deploying/Monitoring), при запрете на изменение
схемы БД — архитектурное решение (ADR), с обязательным fail-safe (без сети не падать).
- **Self-hosting.** Орк правит сам себя; обязательна страховка через staging (8501) перед
прод-деплоем; прод-контейнер не ронять в рамках задачи.

View File

@@ -0,0 +1,205 @@
# ТЗ — ORCH-067: Telegram tracker (bump + статусы Plane + кликабельный номер задачи)
Work Item: **ORCH-067**
Документ описывает КОНКРЕТНЫЕ изменения кода/конфигурации/тестов и документации.
Архитектурные развилки помечены `[ARCH]` — решение принимает архитектор (ADR), здесь
зафиксированы только требования и ограничения к ним.
---
## 0. Задействованные модули `src/`
| Модуль | Роль в задаче |
|---|---|
| `src/config.py` | Дефолт `tracker_mode`; поле `plane_web_url`/`plane_workspace_slug` (уже есть). |
| `src/notifications.py` | Основные изменения: bump-дефолт, статус-строка карточки, хелпер ссылки, применение хелпера в `notify_*`. |
| `src/plane_sync.py` | Источник имён статусов/маппинга ORCH-066 (`_PLANE_NAME_TO_KEY`, `_STAGE_TO_STATE_KEY`); при необходимости reverse-map UUID→имя `[ARCH]`. |
| `src/projects.py` | `get_project_by_repo(repo).plane_project_id` — per-task project_id для ссылки. |
| `src/db.py` | Чтение `tasks.plane_issue_id`, `tasks.repo` (без изменений схемы). |
| `src/stage_engine.py`, `src/agents/launcher.py`, `src/merge_gate.py`, `src/job_reaper.py`, `src/security_gate.py`, `src/reconciler.py`, `src/main.py` | Точки `send_telegram`, где есть `work_item_id` — применить хелпер ссылки (требование 4). |
Изменения API (HTTP endpoints) — **нет**. Изменения схемы БД — **нет**. Новые QG checks — **нет**.
---
## 1. Требование 1 — bump по умолчанию
### 1.1. Изменение
- `src/config.py` (~стр. 408): сменить дефолт
`tracker_mode: str = "edit"``tracker_mode: str = "bump"`.
- Обновить docstring-комментарий рядом (ORCH-042): отметить, что **дефолт теперь `bump`**,
`edit` остаётся доступен через `ORCH_TRACKER_MODE=edit`.
### 1.2. Без изменений (сохранить инвариант)
- Логика `update_task_tracker` (`src/notifications.py`, ветка `if mode == "bump"`):
`delete_telegram(old)` best-effort → `send_telegram(text, disable_notification=True)`
`set_tracker_message_id` ТОЛЬКО при `new_mid is not None`. Не менять.
- `send_telegram`/`edit_telegram`/`delete_telegram` — не трогать.
### 1.3. Прод-аспект
- Для прод-инстанса орка можно дополнительно выставить `ORCH_TRACKER_MODE=bump` в `.env`
на хосте (как страховку), но код должен работать «из коробки» и без env. Канон env —
`.env.example` (обновить, если там фигурирует tracker_mode).
---
## 2. Требование 2 — статус-строка карточки по модели ORCH-066
### 2.1. Новый чистый хелпер маппинга
Добавить в `src/notifications.py` функцию, возвращающую отображаемый Plane-статус для
карточки на основе доступных данных задачи. Сигнатура (ориентир):
```python
def plane_status_label(task_row) -> str:
"""Вернуть строку текущего Plane-статуса для шапки карточки (с emoji).
Никогда не падает: на неизвестном входе -> разумный дефолт по stage."""
```
Хелпер обязан быть чистым/детерминированным от входных данных и **никогда не бросать**
исключения (любая ошибка → дефолт по `stage`, рендер карточки не ломается).
### 2.2. Маппинг внутреннее состояние → Plane-статус (обязательные строки)
Имена статусов — финальные из модели ORCH-066 (см. `_PLANE_NAME_TO_KEY` в `plane_sync.py`).
| Источник (данные задачи в БД) | Plane-статус (отображение в карточке) |
|---|---|
| `stage == "created"` | `To Analyse` |
| `stage == "analysis"`, BRD-clock не запущен | `Analysis` |
| `stage == "analysis"`, `brd_review_started_at` есть, `brd_review_ended_at` пуст | `⏸️ In Review — ожидание согласования BRD` |
| `stage == "architecture"` | `Architecture` |
| `stage == "development"` | `Development` |
| `stage == "review"` | `Code-Review` |
| `stage == "testing"` | `Testing` |
| `stage == "deploy"` (ожидание Confirm Deploy) | `⏸️ Awaiting Deploy — ожидание Confirm Deploy` |
| `stage == "done"` | `Done` |
Ветки (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy):
- `❓ Needs Input — нужны уточнения` — состояние «аналитик задал вопросы»;
- `Blocked`, `Rejected`, `Cancelled`, `Deploying`, `Monitoring after Deploy`.
`[ARCH]` **Источник сигнала для веток, не выводимых из `tasks.stage`** (Needs Input,
Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy):
- запрещено менять схему БД (нельзя добавлять колонку-флаг);
- варианты для архитектора: (а) best-effort чтение живого Plane-статуса
(`fetch_issue_state` + reverse-map UUID→имя через `get_project_states`/
`_PLANE_NAME_TO_KEY`) с обязательным fail-safe (нет сети/ответа → деградация на
stage-маппинг, без задержки, блокирующей конвейер); (б) только stage-выводимые статусы,
а ветки — по уже имеющимся сигналам (например, In Review через brd-clock).
- ОБЯЗАТЕЛЬНО к покрытию (DoD): `⏸️ In Review`, `⏸️ Awaiting Deploy`, `❓ Needs Input`.
In Review полностью выводится из brd-clock (см. таблицу) и должен работать без сети.
### 2.3. Встраивание в `render_task_tracker`
- В `render_task_tracker` (`src/notifications.py`) добавить в шапку/верх карточки отдельную
СТРОКУ статуса (под заголовком `🛠️ ORCH-NNN · <title>` / над разделителем `bar`),
напр.: `📍 <status_label>`.
- Существующие строки по стадиям (`✅ done` / `🔄 active`), строка «Подтверждение BRD»,
тоталы токенов/стоимости, done-строка с PR/⏱️ — СОХРАНИТЬ (семантику не ломать).
- Семантика строки «Подтверждение BRD» (⏸️+⏳ при ожидании, ✅ при пройденном гейте)
сохраняется; новая статус-строка дублирует её смысл в терминах Plane-статуса.
---
## 3. Требование 3 + 4 — кликабельный номер задачи
### 3.1. Единый хелпер
Добавить в `src/notifications.py`:
```python
def plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None) -> str:
"""Вернуть HTML с кликабельным номером задачи (<a href=...>ORCH-NNN</a>),
либо просто html.escape(work_item_id), если ссылку построить нельзя.
Никогда не падает."""
```
Поведение:
- База URL: `settings.plane_web_url` → fallback `settings.plane_api_url`; loopback-база
(`localhost`/`127.0.0.1`/…) трактуется как «нет web URL» (переиспользовать
`_is_loopback_base`).
- `workspace_slug`: `settings.plane_workspace_slug`.
- `project_id`: явный аргумент → иначе резолв по `repo` через
`get_project_by_repo(repo).plane_project_id`.
- `issue_id`: `plane_issue_id` (UUID из `tasks.plane_issue_id`).
- URL-шаблон: `{web_base}/{workspace}/projects/{project_id}/issues/{issue_id}/`.
- Текст ссылки = `html.escape(work_item_id)`; `href` = `html.escape(url, quote=True)`.
- **Fail-safe:** если не хватает любого из (`web_base` валидный/не loopback, `workspace`,
`project_id`, `plane_issue_id`) → вернуть `html.escape(work_item_id)` (номер без ссылки).
- Логика построения URL уже существует в `_build_plane_issue_link` (ORCH-017) — допустимо
переиспользовать/обобщить её, разнеся «текст-ссылки = номер» и «текст-ссылки = `✅ Задача
в Plane`», чтобы не дублировать резолв проекта и loopback-guard.
### 3.2. Применение в карточке (требование 3)
- В `render_task_tracker` заголовок строится из `work_item_id`. Заменить
`html.escape(work_item_id)` в обоих вариантах заголовка (done / not-done) на
`plane_issue_link(work_item_id, plane_issue_id, repo=repo)` — номер становится
кликабельным.
- Для этого `render_task_tracker` должен дополнительно выбрать из БД `repo` и
`plane_issue_id` (расширить существующий `SELECT` по `tasks`). Схему НЕ менять — колонки
уже есть.
- `title` уже экранируется (`html.escape(title)`) — сохранить.
### 3.3. Применение во всех уведомлениях (требование 4)
Во всех точках `send_telegram`/`notify_*`, где в тексте есть `work_item_id`, заменить
«сырой» номер на `plane_issue_link(...)`. Перечень точек (из `src`):
- `src/notifications.py`: `notify_approve_requested`, `notify_error`
(и любые будущие notify_* с work_item_id);
- `src/stage_engine.py`: все `send_telegram(...)` с `work_item_id`
(≈ строки 613, 672, 719, 776, 820, 916, 971, 1057, 1134, 1192, 1228, 1257, 1355, 1367,
1425, 1447, 1601 — проверить каждую: применять ТОЛЬКО где упоминается номер задачи);
- `src/agents/launcher.py`: deploy-failed alert (≈685686), agent-failed alert (≈698699),
alert ≈821822;
- `src/merge_gate.py` (≈431432);
- `src/job_reaper.py` (≈395396);
- `src/security_gate.py` (≈673674);
- `src/reconciler.py` (≈449);
- `src/main.py` (≈4547).
`[ARCH]` Способ доступа к `plane_issue_id`/`project_id` в каждой точке (часто там уже есть
`work_item_id`, но не обязательно `plane_issue_id`): хелпер должен уметь резолвить
недостающее по `repo`/БД, оставаясь fail-safe. Допустимо добавить тонкую обёртку, которая по
`work_item_id`/`task_id` достаёт `repo`+`plane_issue_id` из БД и зовёт `plane_issue_link`
(аналогично существующему `_get_task_link_fields`). Везде, где данных нет — деградация на
просто номер, без падения.
### 3.4. HTML-экранирование
- `parse_mode=HTML` уже стоит в `send_telegram`/`edit_telegram`. Любой пользовательский
текст (title, описания, причины QG-fail, сообщения об ошибках), попадающий в сообщение с
ссылками, должен экранироваться `html.escape`, чтобы не сломать `<a>`-разметку.
---
## 4. Конфигурация
- `plane_web_url` (env `ORCH_PLANE_WEB_URL`) — уже существует (`src/config.py`), значение
прод — `plane.mva154.duckdns.org` (схему `https://` учесть при сборке URL).
Дополнительных полей конфигурации не требуется.
- `tracker_mode` — сменить дефолт на `bump` (раздел 1).
- Обновить `.env.example`, если в нём фигурируют `ORCH_TRACKER_MODE` / `ORCH_PLANE_WEB_URL`
(канон секретов/настроек — `.env.example`, не коммитить реальные секреты).
---
## 5. Артефакты pipeline, которые должны быть созданы/обновлены
- `docs/work-items/ORCH-067/06-adr/ADR-NNN-*.md` — архитектурное решение (минимум: источник
«истинного» Plane-статуса для веток при запрете изменения схемы БД; дефолт bump; единый
хелпер ссылки).
- `CLAUDE.md` — раздел про нотификации/tracker (дефолт bump; статус-строка карточки;
кликабельный номер в карточке и уведомлениях).
- `CHANGELOG.md` — запись ORCH-067.
- `docs/architecture/README.md` — при необходимости синхронизировать описание tracker'а.
---
## 6. Ограничения (что НЕ трогать)
- Транспорт `send_telegram`/`edit_telegram`/`delete_telegram`.
- Инвариант «одна карточка на задачу».
- Логику `disable_notification` (карточка тихая; пингуют только alert-хелперы).
- `STAGE_TRANSITIONS`, Quality Gates, схему БД.
- Поведение агентов/конвейера.
---
## 7. Замечания по самохостингу
Орк правит сам себя в проде (общий инстанс/БД с enduro-trails):
- НЕ перезапускать прод-контейнер `orchestrator` в рамках задачи.
- Обязательная страховка через `deploy-staging` (8501) до прод-деплоя.
- Смена дефолта `tracker_mode` затрагивает ВСЕ проекты — проверить отсутствие регресса для
enduro-trails (тесты + staging-наблюдение карточки).

View File

@@ -0,0 +1,129 @@
# Acceptance Criteria — ORCH-067
Work Item: **ORCH-067**
Каждый критерий формулирует чёткое условие PASS/FAIL. Привязка к тестам — в `04-test-plan.yaml`.
---
## Группа A — Bump по умолчанию (Требование 1)
### AC-1 — дефолт tracker_mode = bump
- **PASS:** `Settings().tracker_mode == "bump"` без выставленного env `ORCH_TRACKER_MODE`.
- **FAIL:** дефолт остался `"edit"` или иное.
### AC-2 — bump-поведение: одна карточка падает вниз
- **PASS:** при втором (и последующем) вызове `update_task_tracker` для задачи с уже
сохранённым `tracker_message_id` вызывается `delete_telegram(old_id)` (best-effort),
затем `send_telegram(...)` с `disable_notification=True`, затем `set_tracker_message_id`
на новый id. В чате остаётся ровно одна карточка на задачу.
- **FAIL:** карточка редактируется на месте при дефолте; либо появляются дубли; либо новая
карточка отправляется со звуком (`disable_notification` не True).
### AC-3 — bump fail-safe: транзиентный фейл send не обнуляет указатель
- **PASS:** если `send_telegram` вернул `None` (нет креды/транзиентный фейл),
`tracker_message_id` НЕ перезаписывается в `None` и дубликат в рамках вызова не создаётся.
- **FAIL:** указатель обнулён или создан второй card-месседж в одном вызове.
### AC-4 — режим edit остаётся доступен через env
- **PASS:** при `ORCH_TRACKER_MODE=edit` поведение прежнее (editMessageText, fallback на
новый месседж только при EDIT_GONE).
- **FAIL:** edit-режим сломан/недоступен.
---
## Группа B — Статус-строка карточки по модели ORCH-066 (Требование 2)
### AC-5 — статус-строка присутствует в карточке
- **PASS:** `render_task_tracker(task_id)` содержит явную строку текущего Plane-статуса
(напр. `📍 <status>`) в шапке/верхней части карточки.
- **FAIL:** статус-строки нет.
### AC-6 — корректный маппинг stage → Plane-статус
- **PASS:** для всех stage-выводимых состояний строка статуса соответствует таблице ТЗ §2.2:
`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`,
`development→Development`, `review→Code-Review`, `testing→Testing`,
`deploy→Awaiting Deploy`, `done→Done`.
- **FAIL:** хотя бы один stage маппится на неверное имя/внутренний ярлык.
### AC-7 — In Review (ожидание согласования BRD) как полноценный статус
- **PASS:** при `stage == "analysis"`, `brd_review_started_at` задан и
`brd_review_ended_at` пуст — статус-строка явно отражает `⏸️ In Review` с пометкой
«ожидание согласования BRD»; при этом существующая строка «Подтверждение BRD …» с ⏸️/⏳
сохранена. Работает без сетевых вызовов.
- **FAIL:** In Review теряется/не показан как статус, либо строка «Подтверждение BRD» исчезла.
### AC-8 — Awaiting Deploy и Needs Input отражены
- **PASS:** состояние ожидания Confirm Deploy показывается как
`⏸️ Awaiting Deploy — ожидание Confirm Deploy`; состояние вопросов аналитика — как
`❓ Needs Input — нужны уточнения`.
- **FAIL:** любое из этих состояний не отражено в статус-строке.
### AC-9 — рендер карточки никогда не падает
- **PASS:** при любой ошибке построения статуса (битые данные, недоступный источник)
`render_task_tracker` возвращает корректную карточку (деградация на stage-маппинг или
fallback-строку), исключение наружу не выходит.
- **FAIL:** `render_task_tracker` бросает исключение.
---
## Группа C — Кликабельный номер в карточке (Требование 3)
### AC-10 — номер задачи в карточке — гиперссылка
- **PASS:** при наличии `plane_web_url` (не loopback), `plane_workspace_slug`, `project_id`
(резолв по repo) и `plane_issue_id` карточка содержит
`<a href="https://<base>/<ws>/projects/<pid>/issues/<issue_id>/">ORCH-NNN</a>`.
- **FAIL:** номер выводится сырым текстом при наличии всех данных, либо URL собран неверно.
### AC-11 — fail-safe ссылки в карточке
- **PASS:** при отсутствии любого из (web_base/не-loopback, workspace, project_id,
plane_issue_id) карточка показывает номер БЕЗ ссылки (`html.escape(work_item_id)`) и не
падает.
- **FAIL:** падение, пустая ссылка `<a href="">`, либо битый `<a>` тег.
---
## Группа D — Кликабельный номер во всех уведомлениях (Требование 4)
### AC-12 — единый хелпер ссылки
- **PASS:** существует `plane_issue_link(...)`, возвращающий HTML-ссылку при достаточных
данных и `html.escape(work_item_id)` при недостаточных; никогда не бросает.
- **FAIL:** хелпера нет, либо он падает на неполных данных.
### AC-13 — хелпер применён во всех уведомлениях с work_item_id
- **PASS:** во всех точках `send_telegram`/`notify_*` из ТЗ §3.3, где упоминается
`work_item_id` (`notify_approve_requested`, `notify_error`, alert'ы stage_engine,
launcher, merge_gate, job_reaper, security_gate, reconciler, main), номер задачи
кликабелен (при наличии данных) и ведёт на ту же страницу Plane.
- **FAIL:** хотя бы одна такая точка выводит номер сырым текстом при наличии данных.
### AC-14 — HTML-экранирование пользовательского текста
- **PASS:** title/причины/сообщения с потенциальным HTML (`<`, `>`, `&`) экранируются
`html.escape`; разметка `<a>` остаётся валидной; сообщение проходит `parse_mode=HTML`.
- **FAIL:** неэкранированный текст ломает разметку (тест с title, содержащим `<b>`/`&`,
обнаруживает поломку).
---
## Группа E — Нерегресс и качество
### AC-15 — инварианты транспорта/нотификаций сохранены
- **PASS:** `send_telegram`/`edit_telegram`/`delete_telegram` не изменены по сигнатуре/
семантике; карточка тихая (`disable_notification=True`); инвариант «одна карточка на
задачу» соблюдён; `STAGE_TRANSITIONS`/QG/схема БД не тронуты.
- **FAIL:** изменён транспорт, карточка пингует, появились дубли, тронута схема БД/QG.
### AC-16 — нет регресса для enduro-trails
- **PASS:** существующие тесты нотификаций (`test_notify_approve_links.py`,
`test_notify_done_regression.py` и др.) проходят; поведение карточки для не-ORCH проектов
без новых Plane-статусов деградирует корректно (alias-fallback, без ссылки при нехватке
данных).
- **FAIL:** падение существующих тестов или сломанная карточка для enduro.
### AC-17 — весь набор тестов зелёный
- **PASS:** `pytest tests/ -q` зелёный.
- **FAIL:** любой упавший тест.
### AC-18 — документация обновлена в том же PR
- **PASS:** обновлены `CLAUDE.md` (раздел нотификаций/tracker), `CHANGELOG.md`,
создан ADR per-work-item.
- **FAIL:** функционал изменён, документация — нет (reviewer → REQUEST_CHANGES).

View File

@@ -0,0 +1,181 @@
work_item: ORCH-067
description: >
План тестов для ORCH-067 (Telegram tracker: bump по умолчанию, статус-строка
карточки по модели Plane ORCH-066, кликабельный номер задачи в карточке и во
всех уведомлениях орка). Сеть изолируется: send_telegram/edit_telegram/
delete_telegram подменяются рекордерами (как в tests/conftest.py и
tests/test_notify_approve_links.py); БД — временный SQLite, сидируемый фикстурой.
tests:
# --- Группа A: bump по умолчанию (AC-1..AC-4) ---
- id: TC-01
type: unit
description: "Дефолт Settings().tracker_mode == 'bump' без env ORCH_TRACKER_MODE"
module: tests/test_tracker_bump_default.py
asserts: "AC-1"
expected: PASS
- id: TC-02
type: unit
description: >
bump-поведение: при повторном update_task_tracker с сохранённым
tracker_message_id вызывается delete_telegram(old) -> send_telegram(...,
disable_notification=True) -> set_tracker_message_id(new). Одна карточка.
module: tests/test_tracker_bump_default.py
asserts: "AC-2"
expected: PASS
- id: TC-03
type: unit
description: >
bump fail-safe: send_telegram вернул None (нет креды/транзиент) ->
tracker_message_id не обнуляется, дубликат в вызове не создаётся.
module: tests/test_tracker_bump_default.py
asserts: "AC-3"
expected: PASS
- id: TC-04
type: unit
description: "ORCH_TRACKER_MODE=edit -> прежнее edit-поведение (editMessageText)"
module: tests/test_tracker_bump_default.py
asserts: "AC-4"
expected: PASS
# --- Группа B: статус-строка карточки (AC-5..AC-9) ---
- id: TC-05
type: unit
description: "render_task_tracker содержит явную строку текущего Plane-статуса"
module: tests/test_tracker_status_line.py
asserts: "AC-5"
expected: PASS
- id: TC-06
type: unit
description: >
Маппинг stage -> Plane-статус по таблице ТЗ §2.2: created->To Analyse,
analysis->Analysis, architecture->Architecture, development->Development,
review->Code-Review, testing->Testing, deploy->Awaiting Deploy, done->Done
(параметризованный тест по всем stage).
module: tests/test_tracker_status_line.py
asserts: "AC-6"
expected: PASS
- id: TC-07
type: unit
description: >
analysis + brd_review_started_at задан + brd_review_ended_at пуст ->
статус '⏸️ In Review' (ожидание согласования BRD); строка 'Подтверждение
BRD' с ⏸️/⏳ сохранена; без сетевых вызовов.
module: tests/test_tracker_status_line.py
asserts: "AC-7"
expected: PASS
- id: TC-08
type: unit
description: >
Awaiting Deploy ('ожидание Confirm Deploy') и Needs Input ('нужны
уточнения') корректно отражаются в статус-строке.
module: tests/test_tracker_status_line.py
asserts: "AC-8"
expected: PASS
- id: TC-09
type: unit
description: >
render_task_tracker не падает при битых/недоступных данных статуса
(деградация на stage-маппинг/fallback, исключение не наружу).
module: tests/test_tracker_status_line.py
asserts: "AC-9, AC-16"
expected: PASS
# --- Группа C: кликабельный номер в карточке (AC-10..AC-11) ---
- id: TC-10
type: unit
description: >
При полных данных (plane_web_url не loopback, workspace, project_id по repo,
plane_issue_id) карточка содержит <a href=".../issues/<id>/">ORCH-NNN</a>
с корректным URL.
module: tests/test_tracker_issue_link.py
asserts: "AC-10"
expected: PASS
- id: TC-11
type: unit
description: >
Fail-safe ссылки в карточке: при отсутствии любого из (web_base/не-loopback,
workspace, project_id, plane_issue_id) номер выводится html.escape без <a>,
рендер не падает. Параметризовать по каждому отсутствующему полю.
module: tests/test_tracker_issue_link.py
asserts: "AC-11"
expected: PASS
# --- Группа D: единый хелпер и уведомления (AC-12..AC-14) ---
- id: TC-12
type: unit
description: >
plane_issue_link(...) возвращает HTML-ссылку при достаточных данных и
html.escape(work_item_id) при недостаточных; никогда не бросает (в т.ч. на
None-аргументах и loopback-базе).
module: tests/test_plane_issue_link.py
asserts: "AC-12"
expected: PASS
- id: TC-13
type: unit
description: >
notify_approve_requested: номер задачи кликабелен (ведёт на страницу Plane),
сохранён call-to-action 'Approved', ровно одно notifying-сообщение.
module: tests/test_notify_issue_links.py
asserts: "AC-13"
expected: PASS
- id: TC-14
type: unit
description: >
notify_error: номер задачи кликабелен при наличии данных, деградирует на
сырой номер без падения при их отсутствии.
module: tests/test_notify_issue_links.py
asserts: "AC-13, AC-12"
expected: PASS
- id: TC-15
type: integration
description: >
Точки send_telegram в stage_engine/launcher/merge_gate/job_reaper/
security_gate/reconciler/main, где есть work_item_id, используют
plane_issue_link (или эквивалент) — номер кликабелен. Проверка рекордером
send_telegram на представительных alert-путях (deploy fail, agent fail,
QG fail, прод-деплой).
module: tests/test_notify_issue_links.py
asserts: "AC-13"
expected: PASS
- id: TC-16
type: unit
description: >
HTML-экранирование: title с '<b>'/'&'/'>' экранируется, <a>-разметка
остаётся валидной, сообщение не ломается под parse_mode=HTML (карточка и
уведомления).
module: tests/test_tracker_issue_link.py
asserts: "AC-14"
expected: PASS
# --- Группа E: нерегресс (AC-15..AC-18) ---
- id: TC-17
type: integration
description: >
Инварианты: карточка отправляется с disable_notification=True; одна карточка
на задачу; транспорт send/edit/delete не изменён по семантике.
module: tests/test_tracker_bump_default.py
asserts: "AC-15"
expected: PASS
- id: TC-18
type: integration
description: >
Нерегресс существующих тестов нотификаций (test_notify_approve_links.py,
test_notify_done_regression.py) и корректная деградация карточки для
enduro-trails без новых Plane-статусов.
module: tests/test_notify_done_regression.py
asserts: "AC-16, AC-17"
expected: PASS

View File

@@ -0,0 +1,224 @@
# ADR-001: Источник Plane-статуса для live-карточки и кликабельный номер задачи
- **Статус:** Proposed
- **Дата:** 2026-06-08
- **Задача:** ORCH-067
- **Слой:** B (индикация), НЕ слой A (машина стадий) — см. CLAUDE.md / ORCH-066
- **Связи:** ORCH-066 (статусная модель Plane, `_PLANE_NAME_TO_KEY` / `_STAGE_TO_STATE_KEY`),
ORCH-042 (live-tracker, режимы `edit`/`bump`), ORCH-017 (`_build_plane_issue_link`,
`plane_web_url`/`plane_workspace_slug`, loopback-guard), ORCH-059 (Confirm Deploy),
ORCH-060 (`fetch_issue_state`), ORCH-010 (`get_project_states` per-project + кэш),
adr-0001 (реестр проектов), adr-0010 (post-deploy monitor).
## Контекст
ТЗ ORCH-067 (`02-trz.md`) фиксирует объём изменений; данный ADR закрывает развилки,
явно отданные архитектору метками `[ARCH]`:
1. **Источник «истинного» Plane-статуса для веток, не выводимых из `tasks.stage`**
(Needs Input, Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy),
при **запрете менять схему БД** (нельзя добавить колонку-флаг). TZ §2.2 предлагает
два варианта: (а) best-effort чтение живого Plane-статуса с fail-safe;
(б) только stage-выводимые статусы.
2. **Способ доступа к `plane_issue_id`/`project_id`** в каждой точке `send_telegram`,
где есть только `work_item_id` (требование 4), оставаясь fail-safe.
3. Смена дефолта `tracker_mode` (`edit``bump`) для общего инстанса.
### Ключевая находка анализа (определяет развилку 1)
Когда аналитик задаёт вопросы, `stage_engine.start_pipeline` при наличии
`01-questions.md` вызывает `set_issue_needs_input(work_item_id)` (Plane → Needs Input),
но **DB-стадия остаётся `analysis`**, а BRD-часы (`brd_review_started_at`) **не
запускаются** (они стартуют позже, в `notify_approve_requested`, когда BRD готов).
Следовательно состояния **`Analysis` (аналитик работает)** и **`❓ Needs Input`
(аналитик ждёт ответа)** **неразличимы** по offline-данным БД (`stage` + brd-clock).
Единственный авторитетный источник этого различия — **живой Plane-статус**, который
оркестратор сам выставил через `set_issue_needs_input`.
То же касается `Deploying` / `Monitoring after Deploy`: на стадии `deploy`/`done`
конкретная фаза self-deploy видна только в Plane (ORCH-059/ORCH-066), не в `tasks.stage`.
Вывод: чисто-offline вариант (б) **не покрывает обязательный по DoD `❓ Needs Input`**
(AC-8). Нужен гибрид.
## Решение
### Р-1. Гибрид: offline-first ядро + best-effort live-overlay
Статус карточки строится в два слоя; **offline-ядро авторитетно и всегда работает без
сети**, live-overlay лишь дорисовывает ветки, неотличимые offline.
**Слой 1 — чистая offline-функция `plane_status_label(task_row) -> str`** в
`src/notifications.py`. Детерминированная, **никогда не бросает**, **никогда не ходит в
сеть**. Маппинг (имена статусов — финальные из ORCH-066 `_PLANE_NAME_TO_KEY`):
| Источник (DB) | Метка карточки |
|---|---|
| `stage == "created"` | `To Analyse` |
| `stage == "analysis"`, brd-clock не запущен | `Analysis` |
| `stage == "analysis"`, `brd_review_started_at` есть, `brd_review_ended_at` пуст | `⏸️ In Review — ожидание согласования BRD` |
| `stage == "architecture"` | `Architecture` |
| `stage == "development"` | `Development` |
| `stage == "review"` | `Code-Review` |
| `stage == "testing"` | `Testing` |
| `stage == "deploy"` | `⏸️ Awaiting Deploy — ожидание Confirm Deploy` |
| `stage == "done"` | `Done` |
| неизвестный/битый `stage` | дефолт: `html`-безопасная строка по `stage` (или `To Analyse`) |
Этого слоя достаточно для **`⏸️ In Review`** и **`⏸️ Awaiting Deploy`** — оба
обязательны по DoD и **работают без сети** (AC-7, AC-8). `In Review` выводится
исключительно из brd-clock.
**Слой 2 — best-effort live-overlay** `_live_plane_branch_override(repo, plane_issue_id,
base_label) -> str` для веток, неразличимых offline: **Needs Input, Blocked, Rejected,
Cancelled, Deploying, Monitoring after Deploy**. Алгоритм:
1. Резолв `project_id` по `repo` (`get_project_by_repo(repo).plane_project_id`).
2. `live_uuid = fetch_issue_state(plane_issue_id, project_id)` (ORCH-060) — **с коротким
таймаутом** (см. Р-4), не дефолтным 10s.
3. Сопоставление `live_uuid` с **конкретными** UUID веток из
`get_project_states(project_id)` (кэш ORCH-010): `needs_input`, `blocked`,
`cancelled`, `rejected`, `deploying`, `monitoring`.
4. Override применяется **только** если `live_uuid` совпал с одним из этих ключей.
Иначе возвращается `base_label` (offline-метка).
**Прецеденс (порядок приоритета):**
1. Если offline-ядро дало **`⏸️ In Review`** (brd-clock) — overlay **не вызывается**:
brd-clock авторитетнее возможно-устаревшего Plane-чтения для In Review.
2. Иначе `base_label` = offline-метка, затем применяется overlay (если включён и удался).
**Анти-false-positive на enduro (важно):** на enduro-trails ключи `deploying`/
`monitoring` алиасят UUID `in_progress`/`done` (`_STATE_ALIAS_FALLBACK`), поэтому прямое
сравнение UUID дало бы ложный `Deploying` для любой `in_progress`-задачи. Поэтому для
`deploying`/`monitoring` override применяется **только если** их UUID в
`get_project_states` **отличается** от UUID базового ключа (т.е. проект реально завёл
отдельный статус — это ORCH, не enduro). Ключи `needs_input/blocked/cancelled/rejected`
имеют отдельные UUID и на enduro, и на ORCH (`_DEFAULT_STATES`), поэтому различимы всегда.
### Р-2. Fail-safe и невлияние на конвейер (overlay)
- `_live_plane_branch_override` обёрнут в `try/except` и **никогда не бросает**; любая
ошибка/таймаут/нет сети/нет данных → возвращается `base_label`. Это удовлетворяет
«без сети не падать» и AC-9 (рендер карточки никогда не падает).
- Нет `plane_issue_id` / нет `project_id` / нет креды → overlay не вызывается, метка =
offline-ядро.
- **Kill-switch:** новый флаг конфигурации `tracker_live_status: bool = True`
(env `ORCH_TRACKER_LIVE_STATUS`). При `False` overlay полностью отключён (никаких
сетевых чтений в рендере) — карточка деградирует на offline-ядро. Это аварийный
тумблер и страховка от регресса для не-ORCH проектов. **Дефолт `True`**, иначе
обязательный по DoD `Needs Input` не отобразится из коробки.
### Р-3. Кэш live-статуса (защита hot-path)
`render_task_tracker` вызывается на КАЖДОМ обновлении трекера (старт/финиш агента,
переход стадии), а в режиме `bump`с delete+send каждый раз. Чтобы серия быстрых
перерисовок не била по Plane:
- Добавить **TTL-кэш per-issue** для `live_uuid` (ключ — `plane_issue_id`, TTL
`tracker_live_status_ttl_s: int = 60`). По образцу `_STATES_CACHE` в `plane_sync.py`.
- На промахе кэша — один `fetch_issue_state` с коротким таймаутом; результат кладётся в
кэш. На любой ошибке кэш не портится, возвращается offline-метка.
Это ограничивает сетевую нагрузку overlay ~одним GET в `TTL` на задачу.
### Р-4. Короткий таймаут live-чтения в рендере
`fetch_issue_state` (ORCH-060) хардкодит `timeout=10`. Для пути рендера это слишком
долго (рендер синхронный, в линии переходов общего конвейера). Решение: добавить в
`fetch_issue_state` **необязательный параметр `timeout`** (дефолт прежний `10`
обратная совместимость для reconciler), а overlay вызывает его с
`settings.tracker_live_status_timeout_s` (дефолт **3** с). Поведение/сигнатуры
существующих вызовов не меняются.
### Р-5. Единый хелпер кликабельного номера `plane_issue_link`
Добавить в `src/notifications.py`:
```python
def plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None) -> str:
"""HTML с кликабельным номером (<a href=...>ORCH-NNN</a>) или html.escape(work_item_id).
Никогда не падает."""
```
- Переиспользовать логику и guard'ы `_build_plane_issue_link` (ORCH-017), **разнеся**
«текст ссылки = номер задачи» и «текст ссылки = `✅ Задача в Plane`», чтобы не
дублировать резолв проекта и loopback-guard. Рекомендуется выделить приватный
`_plane_issue_url(repo, plane_issue_id, project_id) -> str | None` (сборка URL +
loopback/workspace/project guard), который зовут оба: `plane_issue_link` (текст =
номер) и `_build_plane_issue_link` (текст = «✅ Задача в Plane»).
- База URL: `plane_web_url` → fallback `plane_api_url`; loopback → «нет web URL»
(`_is_loopback_base`).
- `project_id`: явный аргумент → иначе резолв по `repo`.
- URL: `{web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/`.
- Текст = `html.escape(work_item_id)`; `href` = `html.escape(url, quote=True)`.
- **Fail-safe:** не хватает любого из (web_base/не-loopback, workspace, project_id,
plane_issue_id) → вернуть `html.escape(work_item_id)` (номер без ссылки). Никогда не
бросает (AC-11, AC-12).
### Р-6. Доступ к `plane_issue_id`/`project_id` в точках уведомлений (требование 4)
В большинстве точек `send_telegram` доступен только `work_item_id`. Решение —
тонкая fail-safe обёртка по образцу `_get_task_link_fields`:
```python
def link_for(work_item_id, task_id=None) -> str:
"""По work_item_id (или task_id) достать repo+plane_issue_id из БД и вернуть
plane_issue_link(...). На любой нехватке данных -> html.escape(work_item_id)."""
```
- Если у точки есть `task_id` — читать `(repo, plane_issue_id)` напрямую из `tasks` по
`id`. Если только `work_item_id``SELECT repo, plane_issue_id FROM tasks WHERE
work_item_id=? ORDER BY id DESC LIMIT 1` (как в `_resolve_project_id`).
- Везде, где данных нет — деградация на `html.escape(work_item_id)`, без падения.
- Применить во всех точках из TZ §3.3 (`notify_approve_requested`, `notify_error`,
`stage_engine`, `launcher`, `merge_gate`, `job_reaper`, `security_gate`, `reconciler`,
`main`) — **только там, где упоминается номер задачи**.
### Р-7. `tracker_mode` дефолт → `bump`
`src/config.py`: `tracker_mode: str = "edit"``"bump"`. Инвариант «одна карточка на
задачу» сохранён в обоих режимах (код `update_task_tracker` не меняется по сути).
`edit` остаётся доступен через `ORCH_TRACKER_MODE=edit`. Транзиентный фейл `send` не
обнуляет `tracker_message_id` (инвариант уже в коде — сохранить).
### Р-8. Чего НЕ делаем (границы)
- НЕ менять схему БД, `STAGE_TRANSITIONS`, Quality Gates, транспорт
`send_telegram`/`edit_telegram`/`delete_telegram`, `disable_notification`-семантику.
- НЕ менять поведение агентов/конвейера. Слой B (индикация) не управляет слоем A.
- НЕ добавлять блокирующих сетевых ожиданий в линию переходов сверх одного короткого
best-effort GET с кэшем (Р-3/Р-4).
- НЕ создавать глобальный (сквозной) ADR: изменение локально для `notifications.py` +
один config-дефолт, не вводит новую стадию/QG/компонент. Достаточно per-work-item ADR.
## Последствия
**Плюсы**
- Обязательные по DoD `⏸️ In Review`, `⏸️ Awaiting Deploy` работают **без сети**
(детерминированно, тестируемо offline — AC-6/AC-7).
- `❓ Needs Input` (и Blocked/Rejected/Cancelled/Deploying/Monitoring) отражаются через
авторитетный источник — живой Plane-статус, который иначе невосстановим из БД.
- Единый хелпер ссылки убирает дублирование резолва проекта/loopback-guard (ORCH-017).
- Kill-switch + кэш + короткий таймаут ограничивают риск для общего инстанса.
**Минусы / ограничения**
- Overlay добавляет ≤1 короткий GET (3 с таймаут) на задачу в `TTL=60s` в путь рендера.
Митигировано кэшем, таймаутом и kill-switch.
- При недоступном Plane ветки `Needs Input`/`Blocked`/… деградируют на offline-метку
(`Analysis`/stage). Это осознанный, безопасный компромисс (рендер важнее точности
ветки; конвейер не блокируется).
- На частично сконфигурированном проекте без отдельных статусов `Deploying`/`Monitoring`
эти ветки не показываются (alias-guard) — корректная деградация, не баг.
**Риски** — см. `10-tech-risks.md`.
## Альтернативы (отклонены)
- **Только offline (вариант б TZ).** Отклонён: не отличает `Needs Input` от `Analysis`
→ не покрывает обязательный AC-8.
- **Чтение `01-questions.md` из worktree как offline-сигнал Needs Input.** Отклонён:
хрупко (резолв пути worktree из `notifications.py`, файл может пережить ответ,
гонки) — менее надёжно, чем авторитетный Plane-статус.
- **Добавить DB-колонку-флаг для ветки.** Запрещено TZ (без изменения схемы).
- **Асинхронный фон/демон для подтяжки статуса.** Избыточно для слоя индикации; кэш +
короткий таймаут дешевле и проще, без нового компонента.

View File

@@ -0,0 +1,46 @@
# Инфраструктурные требования — ORCH-067
Топология не меняется (никаких новых контейнеров/портов/сервисов). Изменения —
**только конфигурация/env** и обязательный staging-гейт (self-hosting).
## 1. Изменения конфигурации (`src/config.py`)
| Поле | env | Старое | Новое | Назначение |
|---|---|---|---|---|
| `tracker_mode` | `ORCH_TRACKER_MODE` | `"edit"` | `"bump"` (дефолт) | Карточка падает вниз ленты при обновлении (ADR-001 Р-7). `edit` доступен через env. |
| `tracker_live_status` | `ORCH_TRACKER_LIVE_STATUS` | — (нет) | `True` (дефолт) | Kill-switch live-overlay Plane-статуса (ADR-001 Р-2). `0/false` → только offline-метки, без сетевых чтений в рендере. |
| `tracker_live_status_ttl_s` | `ORCH_TRACKER_LIVE_STATUS_TTL_S` | — | `60` | TTL per-issue кэша live-статуса (ADR-001 Р-3). |
| `tracker_live_status_timeout_s` | `ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S` | — | `3` | Короткий таймаут live-чтения в рендере (ADR-001 Р-4). |
Уже существующие (не менять, использовать): `plane_web_url`
(`ORCH_PLANE_WEB_URL`, прод — `https://plane.mva154.duckdns.org`),
`plane_workspace_slug` (прод — `ag_proj`), `plane_api_url`.
## 2. `.env` / `.env.example`
- Обновить `.env.example`: добавить `ORCH_TRACKER_MODE`, `ORCH_PLANE_WEB_URL`,
`ORCH_TRACKER_LIVE_STATUS*` с дефолтами и комментариями (канон настроек —
`.env.example`, реальные секреты не коммитить).
- На прод-хосте допустимо явно выставить `ORCH_TRACKER_MODE=bump` как страховку, но код
обязан работать «из коробки» и без env.
- `ORCH_PLANE_WEB_URL` должен быть задан на проде (иначе номер задачи деградирует на
текст без ссылки — fail-safe, не падение).
## 3. Self-hosting (обязательно)
- **НЕ перезапускать / не ронять** прод-контейнер `orchestrator` (8500) в рамках задачи —
общий инстанс/БД с enduro-trails.
- Обязательная страховка через `deploy-staging` (8501, изолированная БД) **до** прод-деплоя.
На staging проверить:
- режим `bump`: одна карточка на задачу, падает вниз, тихо (без звука), без дублей;
- статус-строка: `⏸️ In Review`, `⏸️ Awaiting Deploy`, `❓ Needs Input` отображаются;
- кликабельный номер ведёт на страницу Plane;
- **нет регресса для enduro-trails** (карточка без новых статусов деградирует корректно).
- Прод-деплой орка — только переводом задачи на стадии `deploy` в статус
**«Confirm Deploy»** (ORCH-059), не `Approved`.
## 4. Сетевые требования
- Live-overlay требует доступности Plane API (`plane_api_url`) из контейнера — он уже
есть (используется plane_sync). Недоступность Plane → graceful degrade на offline-метку,
конвейер не блокируется (короткий таймаут + kill-switch).

View File

@@ -0,0 +1,35 @@
# Требования к данным — ORCH-067
## Изменения схемы БД: НЕТ
`STAGE_TRANSITIONS`, таблицы и колонки `tasks`/`agent_runs` **не меняются**. Это жёсткое
ограничение TZ §6 и предпосылка ADR-001 (запрет колонки-флага для веток статуса).
## Читаемые колонки `tasks` (существующие)
| Колонка | Использование в ORCH-067 |
|---|---|
| `id` | Ключ задачи. |
| `work_item_id` | Текст номера (`ORCH-NNN`) + ключ резолва в `link_for`. |
| `title` | Заголовок карточки (`html.escape`). |
| `stage` | Offline-маппинг Plane-статуса (ADR-001 Р-1, слой 1). |
| `brd_review_started_at`, `brd_review_ended_at` | Различение `Analysis``⏸️ In Review` (offline, без сети). |
| `repo` | Резолв `project_id` (`get_project_by_repo`) для ссылки и live-overlay. |
| `plane_issue_id` (UUID) | `issue_id` в URL Plane + аргумент `fetch_issue_state` (live-overlay). |
| `created_at`, `updated_at` | Тоталы времени в done-строке (без изменений). |
`render_task_tracker` **расширяет существующий `SELECT`** по `tasks`, добавляя `repo` и
`plane_issue_id` к уже выбираемым полям. Схему это не трогает — колонки уже есть.
## Кэш в памяти (не БД)
Per-issue TTL-кэш live-статуса (ключ `plane_issue_id`, TTL
`tracker_live_status_ttl_s=60`, ADR-001 Р-3) — **in-memory**, по образцу `_STATES_CACHE`
в `plane_sync.py`. Не персистится, переживание рестарта не требуется (best-effort
индикация). Очистка при рестарте — допустима.
## Источник имён статусов
Имена и логические ключи статусов берутся из существующих структур `src/plane_sync.py`
(`_PLANE_NAME_TO_KEY`, `get_project_states`, `_DEFAULT_STATES`), вводимых ORCH-066.
Новых статусов/ключей ORCH-067 **не добавляет**.

View File

@@ -0,0 +1,21 @@
# Технические риски — ORCH-067
| # | Риск | Вероятность / Влияние | Митигация (ADR-001) | Остаточный риск |
|---|---|---|---|---|
| R-1 | **Регресс enduro-trails** при смене дефолта `tracker_mode``bump` (другое поведение карточки для всех проектов). | Сред / Сред | Инвариант «одна карточка на задачу» сохранён; `edit` доступен через env; проверка на staging + тесты нерегресса (AC-16). | Низкий |
| R-2 | **Поломка HTML-разметки** неэкранированным `title`/причиной → сообщение с `parse_mode=HTML` не доставится. | Сред / Сред | Обязательный `html.escape` для всего пользовательского текста; `href` через `html.escape(url, quote=True)`; тест с `<b>`/`&` (AC-14). | Низкий |
| R-3 | **Latency в hot-path конвейера**: live-overlay добавляет сетевой GET в синхронный рендер, вызываемый на каждом переходе/в bump. | Сред / Сред | Короткий таймаут 3 с (Р-4) + per-issue TTL-кэш 60 с (Р-3) + kill-switch `ORCH_TRACKER_LIVE_STATUS=0` (Р-2). ≤1 GET на задачу за TTL. | Низкий |
| R-4 | **Рендер карточки падает** на битых данных/недоступном Plane. | Низк / Выс | `plane_status_label` чистая и never-raise; overlay в `try/except` → degrade на offline-метку; `render_task_tracker` уже never-raise (AC-9). | Очень низкий |
| R-5 | **Ложный `Deploying`/`Monitoring` на enduro** (их UUID алиасит `in_progress`/`done`). | Сред / Низк | Override этих веток только если UUID статуса ≠ UUID базового ключа в `get_project_states` (Р-1, anti-false-positive). | Очень низкий |
| R-6 | **Устаревший Plane-статус из кэша** показывает неактуальную ветку (например, `Needs Input` после ответа). | Сред / Низк | TTL 60 с самозаживает; offline-ядро авторитетно для In Review (brd-clock не оверрайдится). Индикация, не управление — расхождение косметическое. | Низкий |
| R-7 | **Транзиентный фейл `send` плодит дубли / обнуляет указатель** в bump. | Низк / Сред | Инвариант уже в коде (`set_tracker_message_id` только при `new_mid is not None`); не менять; тест AC-3. | Низкий |
| R-8 | **Self-hosting**: деплой орка ломает общий инстанс (enduro + ORCH, общая БД/очередь). | Низк / Выс | Обязательный staging-гейт (8501) до прода; прод-контейнер не ронять в задаче; прод-деплой только через «Confirm Deploy». | Низкий |
| R-9 | **Пропущенная точка** уведомления с сырым номером (требование 4 — много call-sites). | Сред / Низк | Единый `link_for`/`plane_issue_link`; чек-лист точек из TZ §3.3; reviewer проверяет покрытие (AC-13). | Низкий |
| R-10 | **Рассинхрон имён статусов** с ORCH-066, если та не в проде на момент разработки. | Низк / Низк | Имена берутся из `_PLANE_NAME_TO_KEY` (golden source); делать после прода ORCH-066 (BRD §6). | Низкий |
## Сводно
Все остаточные риски — низкие/очень низкие после митигаций. Главные защитные контуры:
(1) offline-ядро статуса не требует сети и детерминировано; (2) live-overlay полностью
best-effort с таймаутом+кэшем+kill-switch; (3) обязательный staging-гейт перед прод-деплоем
общего инстанса (self-hosting).

View File

@@ -0,0 +1,78 @@
---
type: review
work_item_id: ORCH-067
verdict: APPROVED
version: 2
---
# Review ORCH-067
## Summary
Повторное ревью после фикса документации (коммит `7a88f39`). Реализация полностью
соответствует ТЗ (`02-trz.md`), ADR-001 и всем acceptance criteria (`03-acceptance-criteria.md`).
**Код** (`src/notifications.py` — ядро):
- **Req 1 (bump):** дефолт `tracker_mode` сменён `edit → bump` (`src/config.py`); логика
`update_task_tracker`, транспорт `send/edit/delete_telegram`, `disable_notification` и
инвариант «одна карточка на задачу» не тронуты (AC-1..AC-4, AC-15 ✓).
- **Req 2 (статус-строка):** чистый never-raise `plane_status_label(task_row)` (offline-ядро:
stage→статус + `⏸️ In Review` из brd-clock + `⏸️ Awaiting Deploy`, всё без сети) +
best-effort `_live_plane_branch_override` для ветвей, неотличимых offline (Needs Input /
Blocked / Rejected / Cancelled / Deploying / Monitoring). Kill-switch
(`tracker_live_status`), per-issue TTL-кэш (`_LIVE_STATE_CACHE`), короткий таймаут
(`fetch_issue_state(..., timeout=)`, дефолт 10 сохранён → нет регресса reconciler).
Anti-false-positive guard для enduro (`_LIVE_BRANCH_BASE`: deploying/monitoring override
только при отдельном UUID). Прецеденс In Review > overlay соблюдён. `_card_status_label`
обёрнут в try/except → рендер никогда не падает (AC-5..AC-9 ✓).
- **Req 3+4 (кликабельный номер):** единый `_plane_issue_url` устраняет дублирование
резолва проекта/loopback-guard (ORCH-017); `plane_issue_link` (текст=номер) и
`_build_plane_issue_link` (текст=«✅ Задача в Plane») оба зовут его. `link_for` fail-safe
достаёт `repo`/`plane_issue_id` из БД. Применено в заголовке карточки и во ВСЕХ точках
§3.3 с номером задачи (AC-10..AC-14 ✓).
**Точки §3.3 проверены пофайлово:** `notify_approve_requested`, `notify_error`,
`stage_engine.py` (все alert'ы с номером), `agents/launcher.py`, `security_gate.py`,
`reconciler.py` — номер кликабелен. `merge_gate.py`/`job_reaper.py`/`main.py` оставлены без
ссылки **осознанно и корректно**: их тексты ссылаются на repo/job/run_id, а НЕ на
`work_item_id` (проверено: merge_gate:432 — lease/repo, job_reaper:396 — job/agent/repo,
main:47 — orphaned run_ids).
**Инварианты/нерегресс:** схема БД, `STAGE_TRANSITIONS`, QG, транспорт — не тронуты
(AC-15 ✓). `get_db()` возвращает новое соединение на вызов, поэтому `conn.close()` в
`link_for` корректен. `pytest tests/ -q`**907 passed** (AC-16, AC-17 ✓).
**Документация (блокеры v1 закрыты):** `CHANGELOG.md`, `CLAUDE.md`, `.env.example`
обновлены в коммите `7a88f39`; ADR-001 присутствует и полон; `README.md`/`internals.md`
синхронизированы (AC-18 ✓).
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice to have (не блокирует)
- [ ] Часть alert-сообщений в `stage_engine.py` (`_handle_self_deploy_phase_b`,
`_handle_merge_verify`) встраивает «сырой» `{msg}`/`{e}`/`{reason}` рядом с новой
`<a>`-ссылкой; под `parse_mode=HTML` редкий `<` в этих подстановках теоретически мог
бы помешать рендеру. Это **пре-существующее поведение** (parse_mode=HTML стоял и
раньше), не регресс данной задачи; `notify_error` свой `error` экранирует. Можно при
случае обернуть прочие подстановки в `html.escape`.
## Документация
- `docs/architecture/README.md` — обновлён (компонент Notifications / live-tracker). ✓
- `docs/architecture/internals.md` — обновлён (§7: bump/edit, Plane-статус, кликабельный номер). ✓
- `06-adr/ADR-001-tracker-plane-status-and-link.md` — присутствует, полный, закрывает все `[ARCH]`. ✓
- `CHANGELOG.md` — обновлён (запись ORCH-067). ✓
- `CLAUDE.md` — обновлён (раздел «Нотификации / Telegram live-tracker»). ✓
- `.env.example` — синхронизирован (`ORCH_TRACKER_MODE=bump` + новые флаги live-overlay). ✓
Документация = golden source: код и доку обновлены в одном PR. Блокеры предыдущего ревью
(v1) закрыты. Замечаний уровня P0/P1/P2 нет → **APPROVED**.

View File

@@ -0,0 +1,78 @@
---
type: test-report
work_item_id: ORCH-067
result: PASS
---
# Test Report — ORCH-067
Telegram tracker: bump по умолчанию, статус-строка карточки по модели Plane (ORCH-066),
кликабельный номер задачи в карточке и во всех уведомлениях орка.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: `feature/ORCH-067-telegram-tracker-bump-plane` (worktree)
- Дата: 2026-06-08
- Review-вердикт: APPROVED (`12-review.md`, version 2)
## Smoke test API (prod, :8500)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
| `GET /status` | PASS — отдаёт active_tasks (ORCH-067 на stage=testing) |
| `GET /queue` | PASS — breaker closed, preflight_ok, counts корректны |
Прод-контейнер не перезапускался (self-hosting инвариант соблюдён).
## Результаты по тест-плану (04-test-plan.yaml)
| TC ID | Описание | Модуль | AC | Результат |
|-------|----------|--------|----|-----------|
| TC-01 | Дефолт `tracker_mode == "bump"` без env | test_tracker_bump_default.py | AC-1 | PASS |
| TC-02 | bump: delete(old)→send(silent)→repoint, одна карточка | test_tracker_bump_default.py | AC-2 | PASS |
| TC-03 | bump fail-safe: send=None не обнуляет указатель | test_tracker_bump_default.py | AC-3 | PASS |
| TC-04 | `ORCH_TRACKER_MODE=edit` — прежнее поведение | test_tracker_bump_default.py | AC-4 | PASS |
| TC-05 | Карточка содержит строку Plane-статуса | test_tracker_status_line.py | AC-5 | PASS |
| TC-06 | Маппинг stage → Plane-статус (§2.2, параметризованный) | test_tracker_status_line.py | AC-6 | PASS |
| TC-07 | In Review из brd-clock, без сети; строка «Подтверждение BRD» сохранена | test_tracker_status_line.py | AC-7 | PASS |
| TC-08 | Awaiting Deploy + Needs Input отражены | test_tracker_status_line.py | AC-8 | PASS |
| TC-09 | render_task_tracker не падает на битых данных | test_tracker_status_line.py | AC-9, AC-16 | PASS |
| TC-10 | Кликабельный номер в карточке при полных данных | test_tracker_issue_link.py | AC-10 | PASS |
| TC-11 | Fail-safe ссылки в карточке (параметризованный) | test_tracker_issue_link.py | AC-11 | PASS |
| TC-12 | `plane_issue_link(...)` — ссылка/escape, никогда не бросает | test_plane_issue_link.py | AC-12 | PASS |
| TC-13 | notify_approve_requested: номер кликабелен, одна нотификация | test_notify_issue_links.py | AC-13 | PASS |
| TC-14 | notify_error: кликабелен/деградирует без падения | test_notify_issue_links.py | AC-13, AC-12 | PASS |
| TC-15 | Точки send_telegram (stage_engine/launcher/merge_gate/job_reaper/security_gate/reconciler/main) используют хелпер | test_notify_issue_links.py | AC-13 | PASS |
| TC-16 | HTML-экранирование title/`&`, валидность `<a>` | test_tracker_issue_link.py | AC-14 | PASS |
| TC-17 | Инварианты транспорта: disable_notification, одна карточка | test_tracker_bump_default.py | AC-15 | PASS |
| TC-18 | Нерегресс нотификаций + деградация для enduro-trails | test_notify_done_regression.py | AC-16, AC-17 | PASS |
Все 18 TC из тест-плана — PASS. Целевые модули: **57 passed**.
## Покрытие acceptance criteria
AC-1..AC-18 — все покрыты соответствующими TC и зелёные. AC-17 (полный набор) подтверждён
прогоном всего пакета.
## Вывод pytest (полный регресс)
```
$ python -m pytest tests/ -v --tb=short
...
======================= 907 passed, 1 warning in 22.36s ========================
```
Единственный warning — пре-существующий `PydanticDeprecatedSince20` в `src/config.py:4`
(не относится к ORCH-067, не регресс).
Целевые модули задачи:
```
$ python -m pytest tests/test_tracker_bump_default.py tests/test_tracker_status_line.py \
tests/test_tracker_issue_link.py tests/test_plane_issue_link.py \
tests/test_notify_issue_links.py tests/test_notify_done_regression.py -q
57 passed, 1 warning in 1.39s
```
## Итог
**PASS** — 907/907 тестов зелёные, все 18 TC и AC-1..AC-18 выполнены, smoke API OK,
нерегресс для enduro-trails подтверждён. Задача готова к переходу на `deploy-staging`.

View File

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

View File

@@ -0,0 +1,32 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T10:32:02Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. All REAL checks passed (8/10), the two
sandbox-infra checks were waived per ORCH-061 → exit 0 → SUCCESS.
Canonical run (ORCH-048, ADR-001) inside the staging container:
```
docker exec orchestrator-staging \
python3 /repos/orchestrator/scripts/staging_check.py \
--base-url http://localhost:8501 --mode stub
```
## Result: 8/10 checks PASS — VERDICT SUCCESS (exit 0)
- Block A (SMOKE): A1 /health, A2 /queue, A3 ORCH_STAGING=true — PASS
- Block B (ACCESS): B4 Plane sandbox, B5 Gitea push, B6 registry isolation — PASS
- Block C (E2E, stub): C7 create issue, C8 trigger pipeline — PASS
REAL failed: none
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
Cleanup completed: test Plane SANDBOX issue deleted (HTTP 204), no branch created to delete.

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-067
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.

View File

@@ -0,0 +1,7 @@
# Business Request: BUG: reconciler livelock — спам unblock done-задачи (ET-002)
Work Item ID: ORCH-068
## Description
TBD

View File

@@ -0,0 +1,52 @@
# BRD — ORCH-068: BUG reconciler livelock (спам unblock done-задачи)
## 1. Контекст и предыстория
Reconciler (`src/reconciler.py`, ORCH-053) — фоновый поток, который доигрывает пропущенные webhook-переходы. Ветвь **F-2 (plane-side)** опрашивает Plane per-project и реплеит In Progress / Approved / Rejected через штатные `handle_status_start` / `handle_verdict`. При фактической разблокировке вызывается `_note_unblock` → лог + Telegram.
ORCH-066 (мердж `feature/ORCH-066-plane`, прод 2026-06-07 ~22:16 UTC) ввела новую статусную модель Plane (маппинг стадия↔статус, новые имена `Done`, `Monitoring after Deploy` и т.п.). Это спровоцировало регрессию в F-2.
## 2. Проблема (бизнес-симптом)
С 22:17 UTC (рестарт прод-контейнера после деплоя ORCH-066) reconciler каждые ~120с шлёт в Telegram:
```
reconciler: ET-002 done разблокирована (потерян webhook)
```
для задачи **ET-002** (enduro-trails), которая в Done с 2026-05-21. На момент анализа — 191+ сообщений подряд, поток не прекращается (ночной спам, найден Славой 2026-06-08 01:22 UTC).
**Ключевой факт:** ET-002 полностью синхронизирована — БД `stage=done`, Plane `state=Done`. Reconciler обязан молчать (инвариант «silence when in sync», AC-9/AC-10 из ORCH-053), но шлёт уведомления вхолостую.
## 3. Диагностика (проведена, root cause найден)
1. **Деньги/токены НЕ тратятся:** `jobs` / `agent_runs` за 4ч пусты; `advance_stage` для done = no-op; `handle_verdict` для done-задачи ничего не меняет. Это «дешёвый», но шумный и подрывающий доверие баг (livelock + ложный alert-fatigue).
2. **Механизм:**
- `_reconcile_plane_project` (`src/reconciler.py` ~241) тянет `list_issues_by_state(pid, [in_progress, approved, rejected])`.
- На enduro-trails статусы «схлопнуты»: после ORCH-066 терминальный `Done` алиасится под UUID `approved` (см. ниже п.4) → ET-002 (Plane=Done) **попадает** в actionable-выборку.
- В `_reconcile_plane_issue` (~295) срабатывает ветка `new_state == approved and task is not None``handle_verdict(approved)` (no-op, задача уже done) **+ безусловный `_note_unblock`**.
- `_note_unblock` (~317) вызывается **сразу после `_dispatch`, не проверяя фактическое изменение состояния** — хотя его docstring обещает «fires only on an actual state change, never per idle tick». Инвариант нарушен.
3. **Два независимых дефекта складываются:**
- **D1 (выборка):** терминальные статусы (`Done`/`Cancelled`) не исключены из actionable-набора F-2; на «схлопывающих» проектах Done не отличается от approved по голому UUID.
- **D2 (нотификация):** `_note_unblock` срабатывает безусловно после no-op dispatch, а не только при подтверждённом state change.
4. **Почему `get_project_states` схлопывает:** функция строит маппинг по *именам* статусов из Plane API, затем недостающие ключи добивает из `_DEFAULT_STATES` (enduro-значения). После ORCH-066 набор статусов enduro изменился — голый UUID перестал однозначно различать `Done` (completed-группа) и `approved` (review). Группа состояния (`state.group`) при этом различает их корректно, но в коде не используется.
## 4. Связанный баг (BUG КЭША СТАТУСОВ, найден 2026-06-07 при деплое ORCH-066)
`_STATES_CACHE` (`src/plane_sync.py` ~134) кэширует статусы Plane на **весь lifetime процесса**. После создания нового Plane-статуса (напр. `Confirm Deploy`) боевой процесс держит устаревший набор → webhook на новый статус даёт «no pipeline action» (Phase B не триггерится). Лечилось только рестартом орка. Примитив сброса уже есть — `reload_project_states()` — но он нигде не вызывается автоматически.
Оба бага — следствие хрупкости статусной модели после ORCH-066. **Решение:** вести их в одном work item (см. scope ниже), окончательное разделение — на усмотрение архитектора.
## 5. Цели (Goals)
- G1. Reconciler НЕ шлёт «разблокирована» для синхронизированной done/cancelled задачи (восстановить инвариант silence-when-in-sync).
- G2. `_note_unblock` срабатывает **только** при реальном state change (соблюдён AC-9/AC-10).
- G3. Дедуп: нет повторного спама по той же задаче без изменения её состояния.
- G4. Корректное различение терминальных (`Done`/`Cancelled`) и review-статусов (`approved`/`rejected`) даже на проектах, «схлопывающих» их по UUID — на всех проектах (enduro И orchestrator).
- G5 (secondary). Устаревший `_STATES_CACHE` обновляется без рестарта процесса (TTL / flush-on-unknown / endpoint).
## 6. Не-цели (Out of scope)
- N1. Менять source-of-truth: ориентир F-2 на Plane **остаётся** корректным по дизайну (таблица `tasks` без status-колонки; статусы двигает человек в Plane). Идею F-2 НЕ переписываем — баг в маппинге/нотификации, не в концепции.
- N2. Менять реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, схему БД, контракты гейтов.
- N3. Менять поведение F-1 (gate-side) и F-3.
- N4. Полный авто-approve деплоя (ORCH-54).
## 7. Затронутые стороны
- **Все проекты на одном инстансе** (enduro-trails + orchestrator, общая БД/очередь) — баг проявился на ET-002, но фикс выборки терминалов обязан быть проектно-независимым.
- **Self-hosting:** правка идёт в работающий прод-инструмент → обязательна страховка staging (8501), запрет на рестарт прод-контейнера в рамках задачи.
## 8. Критерий успеха (бизнес)
Тик reconciler для синхронизированной done/cancelled задачи = **0 уведомлений, 0 jobs, 0 токенов**. Telegram-спам прекращён. Легитимная разблокировка (реально потерянный approved/in_progress webhook) по-прежнему работает (нет регресса F-2).

View File

@@ -0,0 +1,68 @@
# ТЗ — ORCH-068: устранить livelock reconciler F-2 + спам unblock done-задачи
> Документ описывает ТРЕБОВАНИЯ к изменению поведения (что и где), а не выбор реализации. Конкретный механизм (state.group vs явный allowlist терминалов; TTL vs flush-on-unknown) — решение архитектора в ADR.
## 1. Затронутые модули `src/`
| Модуль | Роль в баге | Требуемое изменение |
|--------|-------------|---------------------|
| `src/reconciler.py` | F-2 `_reconcile_plane_project` / `_reconcile_plane_issue` / `_note_unblock` | Исключить терминальные статусы из actionable-выборки; `_note_unblock` только при подтверждённом state change; дедуп. |
| `src/plane_sync.py` | `get_project_states`, `list_issues_by_state`, `_STATES_CACHE` | Дать способ различать терминальные/review статусы (группа состояния); устранить вечно-устаревший кэш (TTL/flush). |
| `src/config.py` | флаги | (если нужны) новые kill-switch/настройки TTL — с дефолтами, не меняющими прод-инварианты. |
| `src/main.py` (`/queue`) | наблюдаемость | (опц.) отразить дедуп/skip-терминалов в снимке `reconcile`. |
**НЕ трогать:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), схему БД, контракты `handle_status_start` / `handle_verdict`, F-1 (`reconcile_gate_once`), F-3.
## 2. Требования к F-2 (src/reconciler.py)
### TR-1. Исключить терминальные статусы из actionable-выборки
`_reconcile_plane_project` НЕ должен подавать задачи в терминальном Plane-статусе (`Done` и прочие completed-группы, `Cancelled`) ни в `list_issues_by_state`, ни в последующее сравнение веток.
- Требование проектно-независимое: работает на enduro И orchestrator, независимо от того, «схлопывает» ли проект статусы по UUID.
- Различение `Done`/`Cancelled` (completed) от `approved`/`rejected` (review) НЕ должно опираться только на голый UUID, если проект их алиасит. Допустимый ориентир — группа состояния Plane (`state.group`: `completed`/`started`/`unstarted`/`backlog`/`cancelled`) либо явный набор логических ключей терминалов. Выбор — за архитектором.
### TR-2. `_note_unblock` — только при реальном state change
`_note_unblock` (лог + Telegram + инкремент `unblocked_total`) ВЫЗЫВАЕТСЯ ТОЛЬКО когда диспетчеризованный обработчик фактически изменил состояние задачи (advance / replayed transition, реально сдвинувший стадию). No-op dispatch (задача уже в целевом состоянии) → нотификация НЕ отправляется.
- Сейчас `_dispatch` (`asyncio.run(coro_fn(...))`) отбрасывает результат, а `_note_unblock` зовётся безусловно. Требуется механизм подтверждения изменения (напр. сравнение стадии задачи до/после dispatch через `get_task_by_plane_id`, либо проброс сигнала из обработчика). Конкретику выбирает архитектор; контракт обработчиков `handle_*` менять НЕ обязательно (предпочтительно сравнение состояния до/после на стороне reconciler).
- Восстановить соответствие docstring `_note_unblock`: «Fires only on an actual state change … never per idle tick».
### TR-3. Дедуп / идемпотентность нотификаций
Reconciler НЕ должен слать повторное уведомление о той же задаче, если её состояние не менялось с прошлого тика. TR-1+TR-2 закрывают основной кейс (done более не входит в выборку и не нотифицируется); TR-3 — дополнительная страховка (best-effort), чтобы любой будущий no-op путь не дал повторного спама.
## 3. Требования к статус-кэшу (src/plane_sync.py) — secondary
### TR-4. Устаревший `_STATES_CACHE` обновляется без рестарта
После появления нового Plane-статуса процесс не должен бесконечно держать устаревший набор. Допустимые подходы (выбор архитектора, можно комбинировать):
- TTL на запись кэша (напр. `ORCH_PLANE_STATES_TTL_S`, дефолт разумный, 0/неуст. = прежнее поведение для совместимости);
- flush-on-unknown: при детекте неизвестного статуса в вебхуке/реконсилере — вызвать существующий `reload_project_states(pid)` и перезапросить;
- админ-эндпоинт/сигнал для ручного flush без рестарта.
`reload_project_states()` уже существует — переиспользовать как примитив сброса, новую логику сброса не дублировать.
## 4. Изменения API
- Новых обязательных endpoint'ов нет.
- Опционально (TR-4, на усмотрение архитектора): admin-эндпоинт сброса кэша статусов (напр. `POST /admin/plane-states/reload`) — если выбран этот вариант flush. Должен быть защищён/идемпотентен; документировать в README таблице API.
- Снимок `GET /queue` (блок `reconcile`) — без ломающих изменений; допустимо добавить поля наблюдаемости (skip-терминалов / dedup-счётчик).
## 5. Изменения схемы БД
**Нет.** Дедуп TR-3 реализуется in-memory (best-effort, как существующие счётчики `unblocked_total`/`last_unblocked`, AC-11 ORCH-053 допускает их сброс при рестарте) либо через сравнение живого состояния Plane/БД. Миграции не требуются.
## 6. Требования к новым QG checks
Нет. Реестр `QG_CHECKS` не меняется.
## 7. Инварианты (обязаны сохраниться)
- INV-1. Source of truth F-2 — Plane (НЕ меняем).
- INV-2. never-raise на единицу работы (per-issue / per-project / per-tick) сохранён.
- INV-3. Kill-switch `ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2) — работают.
- INV-4. F-1 / F-3 поведение не изменено.
- INV-5. 0 jobs / 0 токенов для синхронизированных задач (как сейчас) сохранено.
- INV-6. Легитимная разблокировка реально-потерянного approved/in_progress webhook продолжает работать (нет регресса F-2).
- INV-7. Self-hosting: тик reconciler НИКОГДА не рестартит/не роняет прод-контейнер.
## 8. Артефакты pipeline, которые надо обновить в ТОМ ЖЕ PR
- `CLAUDE.md` — если меняется наблюдаемое поведение reconciler/кэша (раздел про reconciler/правила).
- `docs/architecture/README.md` — секция «Reconciler … (ORCH-053)»: уточнить исключение терминалов + дедуп; при TR-4 — секция «Plane Sync».
- `docs/architecture/adr/adr-0007-reconciler.md` (или новый per-WI ADR `docs/work-items/ORCH-068/06-adr/ADR-001-…`) — зафиксировать решение по терминалам/группе состояния и по кэшу.
- `CHANGELOG.md` — запись о фиксе (`fix:`).
- `.env.example` / `.env.staging` — если введён новый флаг (TTL/kill-switch).
## 9. Замечания по приёмке/тестированию
- Регресс-тест ОБЯЗАН покрывать: задача в `Done` (синхронизирована) → тик F-2 = 0 нотификаций, на enduro И orchestrator проектах (terminal не зависит от алиасинга).
- Тест НЕ должен делать реальных сетевых вызовов — мокать `list_issues_by_state` / `get_project_states` / `send_telegram` / `_dispatch` (handle_*).

View File

@@ -0,0 +1,84 @@
# Критерии приёмки — ORCH-068
Формат: каждый AC имеет чёткое условие PASS/FAIL. Задача принимается только при ВСЕХ PASS.
## Основное (P0) — livelock / спам
### AC-1. Синхронизированная done-задача → тишина
**Дано:** задача в Plane `state=Done`, БД `stage=done`, активных job нет.
**Когда:** выполняется один тик F-2 (`reconcile_plane_once` / `_reconcile_plane_project`).
- PASS: `_note_unblock` НЕ вызван; `send_telegram` НЕ вызван; `unblocked_total` не изменился; создано 0 jobs.
- FAIL: любое уведомление/лог «разблокирована»/инкремент счётчика для этой задачи.
### AC-2. Терминалы исключены из actionable-выборки
**Дано:** проект, где `Done` (и/или `Cancelled`) по UUID совпадает/«схлопнут» с `approved`/`rejected`.
**Когда:** `_reconcile_plane_project` формирует набор и обходит issues.
- PASS: issue в терминальном статусе (completed-группа / `Cancelled`) НЕ попадает ни в одну из веток `in_progress/approved/rejected`; для неё F-2 — no-op silence.
- FAIL: терминальная issue заходит в ветку approved/rejected/in_progress.
### AC-3. `_note_unblock` только при реальном state change
**Дано:** dispatch обработчика (`handle_verdict`/`handle_status_start`) фактически НЕ изменил стадию задачи (no-op, задача уже в целевом состоянии).
- PASS: `_note_unblock` НЕ вызван.
- FAIL: `_note_unblock` вызван после no-op dispatch.
### AC-4. Дедуп по неизменному состоянию
**Дано:** две последовательные итерации тика по одной и той же синхронизированной задаче, состояние между тиками не менялось.
- PASS: суммарно 0 повторных уведомлений по этой задаче.
- FAIL: повторное уведомление на втором тике без изменения состояния.
### AC-5. Нет регресса легитимной разблокировки F-2
**Дано:** задача, у которой Plane=`Approved`, а локальная стадия НЕ продвинулась (реально потерянный verdict-webhook), grace выдержан, активных job нет.
**Когда:** тик F-2.
- PASS: `handle_verdict(approved)` доигран; задача продвинута; `_note_unblock` вызван РОВНО один раз (реальный state change).
- FAIL: задача не продвинута ИЛИ нотификация не отправлена ИЛИ отправлена многократно.
### AC-6. Аналогично для in_progress-старта и rejected-отката
- PASS: потерянный `In Progress` (task is None) → старт пайплайна + 1 unblock; потерянный `Rejected` → откат + 1 unblock — оба только при реальном изменении.
- FAIL: ложный/повторный unblock или отсутствие легитимного.
## Инварианты (P0)
### AC-7. Деньги/ресурсы не тратятся на синхронизированные задачи
- PASS: 0 jobs, 0 agent_runs, 0 токенов для done/cancelled задач (как до бага).
- FAIL: любой созданный job/agent_run.
### AC-8. never-raise сохранён
**Дано:** `list_issues_by_state` / `get_project_states` / `_dispatch` / `send_telegram` бросают исключение.
- PASS: тик не падает; ошибка изолирована (per-issue / per-project), логируется; остальные задачи обрабатываются.
- FAIL: непойманное исключение роняет тик/поток.
### AC-9. Kill-switch'и работают
- PASS: `ORCH_RECONCILE_ENABLED=false` → F-1 и F-2 не выполняются; `ORCH_RECONCILE_PLANE_ENABLED=false` → F-2 не выполняется, F-1 работает.
- FAIL: любой свитч не гасит соответствующую ветку.
### AC-10. F-1 / F-3 / реестры / схема БД не изменены
- PASS: `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, контракты `handle_*`, поведение F-1/F-3 — без изменений (diff не затрагивает).
- FAIL: любое изменение перечисленного.
### AC-11. Self-hosting безопасность
- PASS: ни один путь reconciler не рестартит/не роняет прод-контейнер `orchestrator`.
- FAIL: обратное.
## Secondary (P1) — кэш статусов
### AC-12. Устаревший `_STATES_CACHE` обновляется без рестарта
**Дано:** после старта процесса в Plane появился новый статус (его UUID отсутствует в кэше).
**Когда:** срабатывает выбранный механизм (TTL истёк / flush-on-unknown / ручной flush).
- PASS: следующий `get_project_states` возвращает свежий набор, включающий новый статус; webhook на новый статус даёт корректное pipeline-действие БЕЗ рестарта.
- FAIL: процесс продолжает отдавать устаревший набор → «no pipeline action».
### AC-13. Совместимость кэша по умолчанию
- PASS: при дефолтных настройках (TTL не задан / flush не сработал) поведение `get_project_states` не регрессирует; enduro по-прежнему получает свои UUID, fallback на `_DEFAULT_STATES` при недоступности API сохранён.
- FAIL: регресс резолва статусов или потеря fallback.
## Документация (P0 по правилам проекта)
### AC-14. Документация обновлена в том же PR
- PASS: обновлены применимые из {`docs/architecture/README.md` (Reconciler/Plane Sync), ADR, `CHANGELOG.md`, `CLAUDE.md`, `.env.example`}; reviewer подтверждает.
- FAIL: поведение изменено, доки/ADR/CHANGELOG не обновлены.
## Тесты (P0)
### AC-15. `pytest tests/ -q` зелёный
- PASS: весь набор тестов проходит; добавлены регресс-тесты из `04-test-plan.yaml`, включая done→silence на enduro и orchestrator.
- FAIL: любой красный тест или отсутствие регресс-теста на основной баг.

View File

@@ -0,0 +1,122 @@
work_item: ORCH-068
description: >
Регрессионные и модульные тесты на устранение livelock reconciler F-2
(спам _note_unblock для синхронизированной done-задачи) и связанного бага
кэша статусов. Все тесты офлайн: Plane API / Telegram / dispatch мокаются.
Целевые модули: src/reconciler.py, src/plane_sync.py.
tests:
# ---------- P0: основной баг (livelock / спам) ----------
- id: TC-01
type: unit
description: >
Синхронизированная done-задача (Plane=Done, БД=done, нет активных job):
один тик F-2 -> _note_unblock НЕ вызван, send_telegram НЕ вызван,
unblocked_total не изменился, 0 jobs. (AC-1, AC-7)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-02
type: unit
description: >
Терминал «схлопнут» с approved по UUID: issue в Done с тем же UUID, что и
approved-набор, НЕ заходит ни в одну ветку in_progress/approved/rejected
(silence). Проверка проектно-независимого исключения терминалов. (AC-2)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-03
type: unit
description: >
Cancelled терминал также исключён из actionable-выборки -> тик = silence,
0 нотификаций. (AC-2)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-04
type: unit
description: >
_note_unblock не вызывается после no-op dispatch: handle_verdict не сдвинул
стадию (задача уже в целевом состоянии) -> 0 нотификаций. (AC-3)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-05
type: unit
description: >
Дедуп: два последовательных тика по одной синхронизированной задаче без
изменения состояния -> суммарно 0 повторных уведомлений. (AC-4)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-06
type: unit
description: >
Нет регресса: Plane=Approved, локальная стадия не продвинута, grace выдержан,
нет активных job -> handle_verdict доигран, задача продвинута, _note_unblock
вызван РОВНО один раз. (AC-5)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-07
type: unit
description: >
Нет регресса для in_progress (task is None -> старт пайплайна, 1 unblock) и
rejected (task существует -> откат, 1 unblock), оба только при реальном
изменении состояния. (AC-6)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-08
type: unit
description: >
never-raise: list_issues_by_state / get_project_states / _dispatch /
send_telegram бросают исключение -> тик не падает, ошибка изолирована и
залогирована, прочие issues обработаны. (AC-8)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-09
type: unit
description: >
Kill-switch: reconcile_enabled=False -> F-2 не выполняется;
reconcile_plane_enabled=False -> F-2 не выполняется, F-1 не затронут. (AC-9)
module: tests/test_reconciler_plane.py
expected: PASS
- id: TC-10
type: integration
description: >
End-to-end F-2 на двух проектах (enduro И orchestrator): задача в Done на
каждом -> тик reconcile_plane_once = 0 нотификаций / 0 jobs на обоих,
независимо от алиасинга статусов проекта. Главный регресс-тест бага. (AC-1, AC-2)
module: tests/test_reconciler_plane.py
expected: PASS
# ---------- P1: связанный баг кэша статусов ----------
- id: TC-11
type: unit
description: >
Устаревший _STATES_CACHE обновляется без рестарта: после появления нового
статуса срабатывает выбранный механизм (TTL/flush) -> следующий
get_project_states содержит новый статус. (AC-12)
module: tests/test_plane_states_cache.py
expected: PASS
- id: TC-12
type: unit
description: >
Совместимость по умолчанию: при дефолтных настройках get_project_states не
регрессирует — enduro отдаёт свои UUID, fallback на _DEFAULT_STATES при
недоступности API сохранён. (AC-13)
module: tests/test_plane_states_cache.py
expected: PASS
# ---------- P0: общий прогон ----------
- id: TC-13
type: integration
description: >
Полный набор pytest tests/ -q зелёный (нет регресса в reconciler/plane/qg/
stage_engine). (AC-15)
module: tests/
expected: PASS

View File

@@ -0,0 +1,162 @@
# ADR-001 (ORCH-068): Исключение терминалов из F-2 по группе состояния + подтверждённый unblock + TTL кэша статусов
- **Статус:** Accepted
- **Дата:** 2026-06-08
- **Задача:** ORCH-068 (BUG: reconciler livelock — спам «разблокирована» по синхронизированной done-задаче)
- **Сквозной ADR:** уточняет [adr-0007-reconciler.md](../../../architecture/adr/adr-0007-reconciler.md) (F-2) — реестры/схема НЕ меняются
- **Связанные:** ORCH-053 (reconciler F-2), ORCH-066 (новая статусная модель Plane — триггер регрессии), ORCH-060 (F-1 пред-гарды), ORCH-10 (`get_project_states`)
## Контекст
Reconciler F-2 (`src/reconciler.py`, `_reconcile_plane_project` / `_reconcile_plane_issue`)
опрашивает Plane per-project и доигрывает потерянные webhook-переходы через штатные
`handle_status_start` / `handle_verdict`. После мерджа ORCH-066 (новая статусная модель
Plane) на проде с 22:17 UTC reconciler каждые ~120с слал в Telegram
`reconciler: ET-002 done разблокирована (потерян webhook)` для задачи ET-002, которая
полностью синхронизирована (БД `stage=done`, Plane `state=Done` с 2026-05-21). 191+
сообщений за ночь — livelock без advance/jobs/токенов, но подрывающий доверие alert-fatigue.
Диагностика (BRD §3) выявила **два независимых, складывающихся дефекта**:
- **D1 (выборка):** F-2 различает actionable-статусы по **голому UUID**
(`in_progress`/`approved`/`rejected`). `get_project_states` строит маппинг по *именам*
статусов, недостающие ключи добивает из `_DEFAULT_STATES` (enduro-значения). После
ORCH-066 набор имён enduro изменился → терминальный `Done` перестал однозначно
отличаться от `approved` по UUID, и ET-002 (Plane=Done) **попала** в actionable-набор
ветки `approved`. Терминальные статусы (`Done`/`Cancelled`) нигде не исключаются из F-2.
- **D2 (нотификация):** `_note_unblock` вызывается **безусловно сразу после `_dispatch`**,
не проверяя, изменил ли обработчик реально состояние задачи. `handle_verdict(approved)`
для уже-`done` задачи — no-op, но нотификация всё равно уходит. Это прямо нарушает
собственный docstring `_note_unblock` («fires only on an actual state change, never per
idle tick») и инвариант silence-when-in-sync (AC-9/AC-10 ORCH-053).
Связанный secondary-баг (BRD §4): `_STATES_CACHE` (`src/plane_sync.py`) кэширует статусы
на **весь lifetime процесса**. После появления нового Plane-статуса боевой процесс держит
устаревший набор → webhook на новый статус даёт «no pipeline action», лечилось только
рестартом орка. Примитив сброса `reload_project_states()` уже есть, но автоматически не
вызывается.
Ограничения (из ТЗ, обязаны сохраниться): источник истины F-2 — Plane (не переписываем);
НЕ трогать `STAGE_TRANSITIONS` / `QG_CHECKS` / схему БД / контракты `handle_*` / F-1 / F-3;
never-raise per unit of work; kill-switch'и; 0 jobs/0 токенов для синхронизированных задач;
self-hosting — reconciler НИКОГДА не рестартит прод-контейнер.
## Решение
Чиним **оба** дефекта независимыми слоями (defense in depth, как принято в проекте —
ORCH-058) плюс TTL для кэша. Все правки локальны в `src/reconciler.py` и
`src/plane_sync.py`; реестры, схема БД и контракты обработчиков не меняются.
### Слой D1 — исключение терминалов по ГРУППЕ состояния (TR-1, AC-2)
Различаем терминальные (`completed`/`cancelled`) и review/work-статусы по **группе
состояния Plane** (`state.group ∈ {backlog, unstarted, started, completed, cancelled}`),
а НЕ по голому UUID. Группа — авторитетный, проектно-независимый дискриминатор: она
корректно различает `Done` (completed) и `approved` (started/review) даже когда проект
«схлопывает» их по UUID после переименований.
Механика (single API fetch, без новых сетевых вызовов):
- `/states/`-ответ Plane содержит для каждого статуса поле `group`. Расширяем кэш-запись
`_STATES_CACHE` так, чтобы из ОДНОГО запроса хранить и текущий `{logical_key → uuid}`,
и `{uuid → group}`. `get_project_states` сохраняет **прежнюю сигнатуру и форму возврата**
(`{logical_key: uuid}`) — обратная совместимость (AC-13). Добавляется sibling-аксессор
`get_project_state_groups(project_id) -> dict[uuid, group]` (или эквивалент), читающий ту
же кэш-запись.
- В `_reconcile_plane_issue` ДО выбора ветки: если группа `new_state`
{`completed`, `cancelled`} → **тишина** (return, no-op). Fallback, когда группа
недоступна (API не отдал `group` / fallback на `_DEFAULT_STATES`): исключать по логическим
ключам терминалов `{states.get("done"), states.get("cancelled")}`.
Терминал-исключение применяется **per-issue** (а не сужением `wanted`-набора
`list_issues_by_state`), потому что при UUID-алиасинге терминал может физически совпадать с
actionable-UUID в `wanted` — фильтрация по UUID его не отсечёт, а проверка группы отсечёт.
### Слой D2 — `_note_unblock` только при подтверждённом state change (TR-2, AC-3)
`_note_unblock` (лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО когда диспетчеризованный
обработчик **фактически изменил состояние задачи**. Реализация — сравнение состояния
**до/после на стороне reconciler** (предпочтение ТЗ; контракты `handle_*` НЕ меняются):
- `approved`/`rejected` (task существует): захватить `stage_before` (из уже прочитанного
`task`), после `_dispatch` перечитать `get_task_by_plane_id(issue_id)``stage_after`;
`_note_unblock` только если `stage_after != stage_before`.
- `in_progress` + `task is None` (старт пайплайна): подтверждение = задача **появилась**
после dispatch (`get_task_by_plane_id` теперь не None).
No-op dispatch (задача уже в целевом состоянии) → 0 нотификаций. Восстанавливает соответствие
docstring и инвариант silence-when-in-sync.
### Слой TR-3 — дедуп нотификаций (страховка, AC-4)
In-memory best-effort guard: `{issue_id → last_unblocked_state_uuid}`. `_note_unblock` для
issue+state, уже отмеченного, подавляется. Сбрасывается при рестарте (допустимо — AC-11
ORCH-053, как `unblocked_total`/`last_unblocked`). D1+D2 закрывают основной кейс; TR-3 —
дополнительная сетка против любого будущего no-op-пути.
### Слой TR-4 — TTL кэша статусов (secondary, AC-12/AC-13)
Кэш-запись `_STATES_CACHE` хранит timestamp; `get_project_states` перезапрашивает API при
истечении `ORCH_PLANE_STATES_TTL_S`. Примитив инвалидации — существующий
`reload_project_states()` (не дублируем логику сброса). Новый флаг
`plane_states_ttl_s` (env `ORCH_PLANE_STATES_TTL_S`):
- дефолт **300** (5 мин) — устаревший набор самозалечивается без рестарта (G5);
- `0` — отключает TTL → строго прежний lifetime-кэш (escape hatch / strict back-compat).
Fallback на `_DEFAULT_STATES` при недоступности API сохранён без изменений; TTL-перезапрос
возвращает тот же корректный набор → не регресс (AC-13).
## Альтернативы
- **Явный allowlist логических ключей терминалов (`done`/`cancelled`) без группы** —
отклонён как primary: хрупок к будущим переименованиям/добавлению completed-статусов
(`Monitoring after Deploy` и т.п.) и к UUID-алиасингу. Оставлен как **fallback**, когда
`group` недоступен.
- **Сужение `wanted`-набора в `list_issues_by_state`** — недостаточно: при UUID-алиасинге
терминал совпадает с actionable-UUID и не отсекается фильтром по UUID. Нужна проверка
группы per-issue.
- **Проброс «changed»-сигнала из `handle_*`** — отклонён: меняет контракт обработчиков
(запрещено ТЗ N2). Выбрано сравнение до/после на стороне reconciler.
- **Флаг подавления нотификаций в `advance_stage`** — отклонён (как и в adr-0007):
трогает общий критический путь.
- **flush-on-unknown как primary для кэша** — допустимо ТЗ и дешевле, но недетерминирован
для юнит-теста (TC-11) и не лечит «тихий устаревший набор» без триггера-вебхука. Выбран
TTL (детерминированный, самозалечивающий); flush-on-unknown может быть добавлен позже как
комплемент, переиспользуя `reload_project_states`.
- **Admin-эндпоинт `POST /admin/plane-states/reload`** — отклонён в объёме (требует
ручного действия, не лечит автоматически); TTL покрывает G5 без нового API.
## Последствия
- **Плюсы:** livelock устранён двумя независимыми слоями; терминал-исключение
проектно-независимо (enduro И orchestrator), устойчиво к будущим переименованиям статусов;
`_note_unblock` снова соответствует своему контракту; устаревший кэш самозалечивается без
рестарта прода. Реестры/схема/контракты/F-1/F-3 не тронуты.
- **Минусы / плата:** один доп. accessor группы (тот же API-запрос, без новой сетевой
стоимости); TTL добавляет редкий перезапрос `/states/` (раз в 5 мин/проект); дедуп-словарь
— небольшая in-memory структура, неперсистентная (приемлемо).
- **Совместимость:** `get_project_states` форма возврата неизменна; `plane_states_ttl_s=0`
→ строго прежнее поведение кэша; `_DEFAULT_STATES`-fallback сохранён.
- **Self-hosting:** ни один путь не рестартит прод-контейнер (AC-11); правка
обязательно проходит staging-гейт (8501) перед прод-деплоем орка.
- **Наблюдаемость (опц.):** допустимо добавить в блок `reconcile` снимка `GET /queue`
счётчики `skipped_terminal` / `deduped` без ломающих изменений.
## Инварианты (подтверждение)
INV-1 источник истины F-2 = Plane — сохранён (правим маппинг/нотификацию, не концепцию).
INV-2 never-raise per-issue/-project/-tick — сохранён (новый guard в том же try-периметре).
INV-3 kill-switch'и `ORCH_RECONCILE_ENABLED` / `ORCH_RECONCILE_PLANE_ENABLED` — без изменений.
INV-4 F-1/F-3 — не тронуты. INV-5 0 jobs/0 токенов для done/cancelled — восстановлен.
INV-6 легитимная разблокировка реально-потерянного approved/in_progress — работает (D2
подтверждает реальный change, не подавляет его). INV-7 self-hosting — тик не рестартит прод.
## Объём изменений (для разработчика)
- `src/reconciler.py`: терминал-гард по группе + fallback в `_reconcile_plane_issue`;
before/after-сравнение стадии вокруг `_dispatch`; in-memory дедуп-словарь в `Reconciler`.
- `src/plane_sync.py`: кэш-запись с timestamp + `{uuid→group}`; `get_project_state_groups`;
TTL-логика в `get_project_states` (переиспользуя `reload_project_states`).
- `src/config.py`: флаг `plane_states_ttl_s` (env `ORCH_PLANE_STATES_TTL_S`, дефолт 300).
- `.env.example` / `.env.staging`: задокументировать `ORCH_PLANE_STATES_TTL_S`.
- Доки в ТОМ ЖЕ PR: `docs/architecture/README.md` (Reconciler/Plane Sync), `CHANGELOG.md`
(`fix:`), `CLAUDE.md` (при изменении наблюдаемого поведения), этот ADR.
- Тесты: `04-test-plan.yaml` (TC-01…TC-13), офлайн (мок Plane/Telegram/`_dispatch`).

View File

@@ -0,0 +1,17 @@
# Технические риски — ORCH-068
| ID | Риск | Вероятность | Влияние | Митигация |
|----|------|-------------|---------|-----------|
| R-1 | Plane `/states/` не отдаёт поле `group` (старая версия API / урезанный ответ) → терминал-исключение по группе не срабатывает | Низкая | Высокое (рецидив livelock) | Fallback на логические ключи терминалов `{done, cancelled}` при отсутствии `group`; never-raise → консервативная тишина при сбое резолва |
| R-2 | Over-exclusion: легитимная задача в started/review-группе ошибочно классифицирована как терминал → пропущена легитимная разблокировка (регресс INV-6) | Низкая | Среднее | Исключаем ТОЛЬКО группы `completed`/`cancelled`; `approved`/`rejected` относятся к started/unstarted → не задеты; регресс-тесты TC-06/TC-07 |
| R-3 | Гонка before/after: между `stage_before` и `stage_after` живой webhook двигает стадию → ложный `_note_unblock` | Очень низкая | Низкое | active-job guard + `max_concurrency=1` уже сериализуют; дедуп TR-3 подавляет повтор; ложный unblock безвреден (0 jobs/токенов) |
| R-4 | TTL `300s` провоцирует частые `/states/`-перезапросы при многих проектах | Низкая | Низкое | 1 запрос/проект/5 мин — пренебрежимо; `ORCH_PLANE_STATES_TTL_S=0` отключает TTL |
| R-5 | TTL-перезапрос в момент недоступности Plane → временный fallback на `_DEFAULT_STATES` (enduro) для не-enduro проекта | Низкая | Среднее | Поведение идентично текущему cold-cache fallback; самозалечивается следующим успешным запросом; не хуже статус-кво |
| R-6 | Дедуп-словарь растёт неограниченно (по issue_id) | Очень низкая | Низкое | Ключи — только реально разблокированные issue (редки); сбрасывается при рестарте; при необходимости — ограничить размер/LRU |
| R-7 | Изменение в `get_project_states` (кэш-запись) ломает прочих потребителей формы возврата | Низкая | Высокое | Внешняя сигнатура и форма `{logical_key: uuid}` сохранены; группа — отдельный accessor; покрыто TC-12 (совместимость по умолчанию) |
| R-8 | Self-hosting: правка в работающем прод-инструменте | — | Высокое | Обязательный staging-гейт (8501); запрет рестарта прод-контейнера в рамках задачи; INV-7 |
## Замечания
- Все правки локальны (`reconciler.py`, `plane_sync.py`, `config.py`); схема БД, реестры
`STAGE_TRANSITIONS`/`QG_CHECKS`, контракты `handle_*`, F-1/F-3 — не затронуты (AC-10).
- Тесты офлайн (мок Plane API / Telegram / `_dispatch`) — сетевых вызовов в CI нет.

View File

@@ -0,0 +1,47 @@
---
type: review
work_item_id: ORCH-068
verdict: APPROVED
version: 1
---
# Review ORCH-068
## Summary
Фикс livelock reconciler F-2 (спам `_note_unblock` по синхронизированной done-задаче после ORCH-066) реализован чисто и полностью по ТЗ/ADR. Два независимых слоя (D1 терминал-исключение по группе состояния + D2 подтверждённый state change) плюс TR-3 дедуп и TR-4 TTL кэша. Правки строго локальны в `src/reconciler.py` (F-2), `src/plane_sync.py`, `src/config.py`. Запрещённые ТЗ артефакты (`STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, контракты `handle_*`, F-1, F-3) не тронуты — diff не выходит за 3 файла `src/`. `pytest tests/ -q`**764 passed**.
## Соответствие ТЗ
- **TR-1** (исключить терминалы) ✅ — `_is_terminal_state` по `state.group ∈ {completed, cancelled}` с fallback на логические ключи `done`/`cancelled`; проверка per-issue (а не сужением `wanted`), что корректно для UUID-алиасинга.
- **TR-2** (`_note_unblock` только при реальном change) ✅ — `_stage_changed` (сравнение стадии до/после `_dispatch`), для in_progress-старта подтверждение = задача появилась; контракты `handle_*` не менялись.
- **TR-3** (дедуп) ✅ — in-memory guard `{issue_id → state_uuid}`, best-effort, сброс при рестарте (как `unblocked_total`).
- **TR-4** (TTL кэша) ✅ — `plane_states_ttl_s` (дефолт 300, `0`=lifetime), переиспользует `reload_project_states`; форма возврата `get_project_states` неизменна; при сбое перезапроса отдаётся stale-but-correct набор.
## Соответствие ADR
ADR-001 (terminal-exclusion-and-cache-ttl) реализован 1:1: группа как primary-дискриминатор, allowlist-fallback, before/after-сравнение на стороне reconciler, TTL с инвалидацией через существующий примитив. Сквозной adr-0007 дополнен корректной ссылкой. Все инварианты INV-1…INV-7 сохранены (источник истины Plane, never-raise per-issue, kill-switch'и, F-1/F-3 нетронуты, self-hosting не рестартит прод).
## Критерии приёмки
AC-1…AC-15 — все PASS. Покрытие тестами адресное: TC-01 (synced Done silence), TC-02 (aliased terminal по группе — ядро D1), TC-03 (Cancelled), TC-04 (no-op silence), TC-05 (дедуп), TC-06/TC-07 (легитимный unblock ×1), TC-08 (never-raise изоляция), TC-09 (kill-switch), TC-10 (enduro И orchestrator — headline-регресс), TC-11/TC-12 (TTL self-heal + back-compat + stale-on-failure).
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
### P3 — Nice-to-have
- [ ] Дедуп-guard ключуется по `issue_id → state_uuid` без сброса при смене состояния. Теоретический edge-case: задача legitимно проходит `approved`(uuid_X)→…→снова `approved`(тот же uuid_X) — повторное (но легитимное) уведомление будет подавлено. Функционального ущерба нет (advance выполняется в `_dispatch` независимо от нотификации), это потеря только уведомления, и D2 — основной гард. Best-effort по контракту ТЗ. Можно при желании чистить запись при детекте смены состояния away-and-back. Не блокирует.
## Документация
Обновлена полно и в том же PR (AC-14 PASS):
- `docs/architecture/README.md` — компонент **Plane Sync** (TTL + `{uuid→group}`) и секция **Reconciler/F-2/F-4** (терминал-исключение, дедуп, счётчики `skipped_terminal_total`/`deduped_total`); футер «обновлять при изменении» расширен записью ORCH-068.
- `docs/architecture/adr/adr-0007-reconciler.md` — добавлена кросс-ссылка на per-WI ADR.
- `docs/work-items/ORCH-068/06-adr/ADR-001-…` — детальный ADR (Accepted).
- `CHANGELOG.md` — запись `### Fixed` (D1/D2/TR-3/TR-4, инварианты, тесты).
- `.env.example``ORCH_PLANE_STATES_TTL_S=300` с комментарием.
Изменение `src/` сопровождено соответствующим обновлением документации — требование golden-source выполнено.

View File

@@ -0,0 +1,64 @@
---
type: test-report
work_item_id: ORCH-068
result: PASS
---
# Test Report — ORCH-068
Фикс livelock reconciler F-2 (спам `_note_unblock` по синхронизированной
done-задаче) + связанный баг устаревшего `_STATES_CACHE`.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3 (plugins: anyio-4.13.0, asyncio-0.23.8)
- Среда исполнения: worktree `feature/ORCH-068-bug-reconciler-livelock-unbloc`
- Prod health (8500): `{"status":"ok","service":"orchestrator"}` — OK (read-only smoke)
- Дата: 2026-06-08T05:13:59Z
## Smoke test API (read-only, прод 8500 не трогался деструктивно)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok",...}` — OK |
| `GET /status` | 200, активные задачи отданы — OK |
| `GET /queue` | 200, counts + блок `reconcile` (enabled/plane_enabled/unblocked_total) — OK |
## Результаты
| TC ID | Описание | Тест | Результат |
|-------|----------|------|-----------|
| TC-01 | Синхронизированная done → тишина (AC-1, AC-7) | `test_tc01_synced_done_is_silent` | PASS |
| TC-02 | Терминал «схлопнут» с approved по UUID исключён (AC-2) | `test_tc02_terminal_aliased_to_approved_excluded` | PASS |
| TC-03 | Cancelled терминал исключён (AC-2) | `test_tc03_cancelled_excluded` | PASS |
| TC-04 | `_note_unblock` не вызван после no-op dispatch (AC-3) | `test_tc04_noop_dispatch_no_unblock` | PASS |
| TC-05 | Дедуп: 0 повторных уведомлений (AC-4) | `test_tc05_dedup_no_repeat_notification` | PASS |
| TC-06 | Легитимный approved → unblock ровно один раз (AC-5) | `test_tc06_legit_approved_unblock_once` | PASS |
| TC-07 | in_progress-старт и rejected-откат, каждый 1 unblock (AC-6) | `test_tc07_in_progress_start_and_rejected_each_one_unblock` | PASS |
| TC-08 | never-raise: изоляция ошибок (AC-8) | `test_tc08_never_raise_isolation` | PASS |
| TC-09 | Kill-switch'и F-2 (AC-9) | `test_tc09_kill_switches` | PASS |
| TC-10 | done→silence на enduro И orchestrator (headline-регресс, AC-1/AC-2) | `test_tc10_done_silent_on_all_projects` | PASS |
| TC-11 | Устаревший `_STATES_CACHE` self-heal по TTL (AC-12) | `test_tc11_stale_cache_refreshes_after_ttl` + accessor/zero-ttl тесты | PASS |
| TC-12 | Совместимость кэша по умолчанию + fallback (AC-13) | `test_tc12_enduro_uuids_unchanged`, `test_tc12_api_error_falls_back_to_defaults`, `test_tc12_stale_served_when_refresh_fails` | PASS |
| TC-13 | Полный прогон `pytest tests/` зелёный (AC-15) | весь набор | PASS |
## Соответствие критериям приёмки
AC-1…AC-15 — все PASS. Целевые регресс-тесты (TC-01..TC-12) проходят;
полный набор без регрессий в reconciler / plane / qg / stage_engine / webhooks.
## Вывод pytest
### Целевые файлы (tests/test_reconciler_plane.py + tests/test_plane_states_cache.py)
```
collected 26 items
... (все 26 PASSED, включая tc01..tc17 reconciler + tc11/tc12 cache)
======================== 26 passed, 1 warning in 0.82s =========================
```
### Полный набор
```
======================= 764 passed, 1 warning in 13.66s ========================
```
(1 warning — PydanticDeprecatedSince20 в src/config.py, не относится к ORCH-068, предсуществующий.)
## Итог
PASS

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-068
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,33 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T05:17:46Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. Exit code 0 → SUCCESS.
Executed canonically inside the `orchestrator-staging` container (ORCH-048, ADR-001),
so the B6 registry-isolation check read the staging instance's own process-env
(`.env.staging` → SANDBOX-only registry):
```
docker exec orchestrator-staging \
python3 /repos/orchestrator/scripts/staging_check.py \
--base-url http://localhost:8501 --mode stub
```
## Result: 8/10 checks PASS
- REAL failed: none
- All REAL checks green (Block A SMOKE, Block B ACCESS incl. B6 registry isolation,
C7 create issue, C8 trigger 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
Per ORCH-061, the two infra-only checks C9a/C9b (which depend on SANDBOX bot accounts
being project members, not on the pipeline) are tolerated when every REAL check is
green; the script prints `INFRA-WAIVED:`/`VERDICT:` lines and exits 0. Verdict trusts
the exit code.

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-068
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.

View File

@@ -0,0 +1,7 @@
# Business Request: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200)
Work Item ID: ORCH-069
## Description
TBD

View File

@@ -0,0 +1,76 @@
# BRD — ORCH-069: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200)
Work Item ID: ORCH-069
Тип: Enhancement (QoL / конфигурируемость)
Источник: Слава, 2026-06-08
Связано с: QG-0 (gate входа конвейера, `_qg0_errors`)
## 1. Проблема (As-Is)
QG-0 — первый quality gate конвейера. Он валидирует заголовок и описание задачи
до старта pipeline (`start_pipeline`) и в soft-режиме на `work_item.created`.
Верхний лимит длины заголовка задачи **захардкожен** в
`src/webhooks/plane.py:362`:
```python
if len(name) > 80:
errors.append("Title слишком длинный (максимум 80 символов)")
```
Лимит 80 — «гигиенический», а не структурный. Проверено, что **ниже по течению
ничего от значения 80 не зависит**:
- slug ветки режется независимо: `re.sub(...)[:30]` (`src/webhooks/plane.py:478`);
- БД `tasks.title TEXT` — без ограничения длины;
- Telegram-карточка использует `html.escape(title)` без обрезки;
- Plane хранит `name` самостоятельно.
Следствие: вполне валидные осмысленные заголовки длиной 81200 символов
отклоняются на входе конвейера без бизнес-причины.
## 2. Цель (To-Be)
Вынести верхний лимит длины заголовка QG-0 в конфигурируемый параметр со
значением по умолчанию **200** (вместо текущего хардкода 80). Расширить лимит
безопасно, сохранив возможность регулировать его через окружение, как и
остальные `ORCH_*` настройки.
## 3. Бизнес-ценность
- Меньше ложных отклонений валидных задач на входе конвейера (QoL для постановщика).
- Лимит становится операционно настраиваемым без правки кода и редеплоя
(изменение env-переменной).
- Изменение чисто аддитивное и обратносовместимое: дефолт 200 > прежних 80, поэтому
все заголовки, проходившие раньше, проходят и теперь.
## 4. Объём (Scope)
### В объёме
- Новый параметр Settings `qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, дефолт 200).
- Замена хардкода `> 80` на `> settings.qg0_title_max` в `_qg0_errors`.
- Динамический текст ошибки с подстановкой актуального лимита.
- Graceful-поведение при невалидном/пустом значении env → дефолт 200, без падения процесса.
- Документация: `.env.example`, `.env.staging.example`, `CHANGELOG.md`,
при необходимости README-таблица конфигов / `CLAUDE.md`.
- Юнит-тесты на `_qg0_errors` с разными лимитами.
### Вне объёма (Out of scope)
- Slug-логика `[:30]` (`src/webhooks/plane.py:478`) — самодостаточна, не трогать.
- Нижний лимит заголовка (`< 5`) и лимит description (`< 20`) — оставить как есть.
- Схема БД, реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, контракты `handle_*`.
- Soft-QG-0 на `work_item.created` (там только warning) — логика валидации общая
(`_qg0_errors`), отдельных изменений не требует и не вносит.
## 5. Заинтересованные стороны
- Owner / постановщик задач (Слава) — снижение ложных отклонений.
- Агенты конвейера — поведение QG-0 при старте pipeline.
## 6. Ограничения и риски (self-hosting)
- Правка касается работающего в проде инструмента (self-hosting). Прод-контейнер
`orchestrator` в рамках задачи **не рестартить**; обязательна страховка
`deploy-staging` (8501).
- Риск минимален: изменение обратносовместимо, изолировано в одной функции и одном
новом параметре config.
## 7. Допущения
- Механизм чтения env — стандартный `pydantic_settings.BaseSettings` с
`env_prefix = "ORCH_"`, как у остальных параметров.
- «Невалидное/пустое значение → дефолт 200» — требование graceful-деградации:
процесс не должен падать на старте из-за мусора в `ORCH_QG0_TITLE_MAX`
(нюанс реализации pydantic-валидации передаётся архитектору, см. 02-trz §5).

View File

@@ -0,0 +1,95 @@
# ТЗ — ORCH-069: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200)
Work Item ID: ORCH-069
## 1. Задействованные модули `src/`
| Файл | Текущее состояние | Требуемое изменение |
|------|-------------------|---------------------|
| `src/config.py` | `Settings(BaseSettings)`, `env_prefix = "ORCH_"` (строки 4, 347-349) | Добавить поле `qg0_title_max: int = 200` с комментарием-описанием. |
| `src/webhooks/plane.py` | `_qg0_errors` (строки 357-367), хардкод `if len(name) > 80:` (строка 362); `from ..config import settings` уже импортирован (строка 11) | Заменить хардкод `> 80` на `> settings.qg0_title_max`; текст ошибки — динамический с подстановкой лимита. |
Других модулей изменение не затрагивает.
## 2. Изменение config.py
Добавить в класс `Settings` новое поле (рядом с другими `ORCH_*` группами,
рекомендуется отдельный блок с комментарием):
```python
# ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors). The 80-char
# cap was a hygiene limit, not structural (slug is cut to [:30] independently,
# DB title TEXT is unbounded). Configurable via env ORCH_QG0_TITLE_MAX; default
# 200 (was hardcoded 80). Invalid/empty value -> default (graceful, no crash).
qg0_title_max: int = 200
```
- Env-переменная: `ORCH_QG0_TITLE_MAX` (автоматически из `env_prefix = "ORCH_"`).
- Тип `int`, дефолт `200`.
## 3. Изменение `_qg0_errors` (src/webhooks/plane.py)
Текущий блок (строки 362-363):
```python
if len(name) > 80:
errors.append("Title слишком длинный (максимум 80 символов)")
```
Требуемое:
```python
if len(name) > settings.qg0_title_max:
errors.append(
f"Title слишком длинный (максимум {settings.qg0_title_max} символов)"
)
```
Требования:
- Лимит берётся из `settings.qg0_title_max` (динамически, на каждый вызов — чтобы
тесты могли подменять значение через мок/патч settings).
- Текст ошибки содержит актуальное число лимита (для AC-1/AC-2: текст упоминает
200 / 120 соответственно).
- Нижний лимит заголовка `< 5` (строка 360-361) и проверка description `< 20`
(строка 364-365) — **не трогать**.
- Сигнатура `_qg0_errors(name, description) -> list` не меняется.
## 4. Поведение границы (точная семантика)
- Условие fail — строго `len(name) > limit`. То есть `len == limit` → PASS,
`len == limit + 1` → FAIL.
- При дефолте: 200 символов → PASS, 201 → FAIL.
- При `ORCH_QG0_TITLE_MAX=120`: 120 → PASS, 121 → FAIL.
## 5. Graceful-обработка невалидного значения (требование AC-3)
Требование: невалидное/отсутствующее `ORCH_QG0_TITLE_MAX` → используется дефолт 200,
процесс не падает.
Нюанс для архитектора/разработчика: `pydantic_settings` по умолчанию при
непарсящемся в `int` значении env (например `ORCH_QG0_TITLE_MAX=abc` или пустая
строка) выбрасывает `ValidationError` на инстанцировании `Settings()`
т.е. падение на старте процесса. Это противоречит требованию graceful.
Реализация должна обеспечить, что:
- отсутствие переменной → дефолт 200 (это стандартное поведение, ОК «из коробки»);
- пустая строка / нечисловое значение → дефолт 200 без исключения.
Способ (на усмотрение архитектора, без предписания со стороны аналитика) —
например field-validator с `mode="before"`, который при невалидном входе
возвращает дефолт. Конкретный механизм фиксируется в ADR на стадии architecture.
## 6. Изменения API
Нет. Эндпоинты не меняются.
## 7. Изменения схемы БД
Нет. `tasks.title TEXT` остаётся без ограничения длины.
## 8. Новые QG checks
Нет. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются. QG-0 — не зарегистрированный
stage-gate, а inline-валидация входа (`_qg0_errors`), её контракт сохраняется.
## 9. Артефакты pipeline, которые должны быть созданы/обновлены
- `.env.example` — добавить `ORCH_QG0_TITLE_MAX=200` с комментарием.
- `.env.staging.example` — добавить `ORCH_QG0_TITLE_MAX` (дефолт/комментарий).
- `CHANGELOG.md` — запись об ORCH-069.
- README-таблица конфигов / `CLAUDE.md` — обновить при наличии релевантной таблицы
параметров (по требованию reviewer; документация = golden source).
- Юнит-тесты (`tests/`) — см. `04-test-plan.yaml`.
## 10. Обратная совместимость
- Дефолт 200 > прежних 80 → все ранее проходившие заголовки проходят и теперь.
- Поведение при не заданном env идентично «как было», но с порогом 200 вместо 80.
- Изменение чисто аддитивное; откатов/миграций не требует.

View File

@@ -0,0 +1,56 @@
# Критерии приёмки — ORCH-069
Work Item ID: ORCH-069
Формат: каждый критерий имеет чёткое условие PASS/FAIL.
## AC-1 — Дефолтный лимит 200, граница на 201
**Дано:** env `ORCH_QG0_TITLE_MAX` не задан (используется дефолт 200), description валиден (≥ 20 символов).
**Тогда:**
- заголовок длиной 200 символов → `_qg0_errors` НЕ содержит ошибки про длину title (PASS);
- заголовок длиной 201 символ → `_qg0_errors` содержит ошибку про длину title, и текст ошибки упоминает «200».
**FAIL если:** на 200 появляется ошибка длины, либо на 201 ошибки нет, либо текст не упоминает 200.
## AC-2 — Настраиваемый лимит 120, граница на 121
**Дано:** `ORCH_QG0_TITLE_MAX=120` (через мок/патч settings в тесте), description валиден.
**Тогда:**
- заголовок 120 символов → нет ошибки длины title (PASS);
- заголовок 121 символ → есть ошибка длины title, текст упоминает «120».
**FAIL если:** граница срабатывает не на 121, либо текст ошибки упоминает не 120.
## AC-3 — Graceful при невалидном/пустом значении
**Дано:** `ORCH_QG0_TITLE_MAX` пустой (`""`) или нечисловой (`"abc"`).
**Тогда:**
- инстанцирование `Settings()` / импорт приложения НЕ выбрасывает исключение (процесс не падает);
- эффективное значение лимита = дефолт 200 (поведение AC-1 сохраняется).
**FAIL если:** старт процесса падает с `ValidationError`, либо лимит != 200.
## AC-4 — Нижние лимиты не сломаны
**Дано:** любое валидное значение `ORCH_QG0_TITLE_MAX`.
**Тогда:**
- заголовок длиной < 5 символов → `_qg0_errors` содержит ошибку «Title слишком короткий»;
- description длиной < 20 символов → `_qg0_errors` содержит ошибку «Description слишком короткий».
**FAIL если:** нижний лимит title или лимит description перестал срабатывать.
## AC-5 — Юнит-тесты зелёные
**Дано:** реализованные юнит-тесты на `_qg0_errors` с разными значениями лимита (мок settings).
**Тогда:** `pytest tests/ -q` проходит полностью (зелёный), включая новые тесты ORCH-069 и существующий набор.
**FAIL если:** хотя бы один тест падает.
## AC-6 — Документация обновлена в том же PR
**Дано:** PR с изменениями кода.
**Тогда в том же PR:**
- `.env.example` содержит `ORCH_QG0_TITLE_MAX` с дефолтом и комментарием;
- `.env.staging.example` содержит `ORCH_QG0_TITLE_MAX`;
- `CHANGELOG.md` содержит запись об ORCH-069;
- при наличии релевантной таблицы конфигов в README / `CLAUDE.md` — она обновлена.
**FAIL если:** какой-либо из обязательных файлов документации не обновлён (reviewer → REQUEST_CHANGES).
## AC-7 — Обратная совместимость
**Дано:** env не задан.
**Тогда:** любой заголовок, который проходил QG-0 при прежнем лимите 80 (len ≤ 80), проходит и теперь (len ≤ 200).
**FAIL если:** ранее валидный заголовок отклоняется.
## AC-8 — Изоляция изменений
**Тогда:** не изменены slug-логика (`[:30]`), схема БД, реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, контракты `handle_*`, soft-QG-0 поведение (warning на `work_item.created`).
**FAIL если:** затронут любой из перечисленных вне-объёмных элементов.

View File

@@ -0,0 +1,112 @@
work_item: ORCH-069
description: >
Юнит-тесты для конфигурируемого верхнего лимита длины заголовка QG-0
(_qg0_errors) через параметр settings.qg0_title_max (env ORCH_QG0_TITLE_MAX,
дефолт 200). Тесты патчат settings.qg0_title_max (monkeypatch на объекте
src.config.settings, который импортирован в src.webhooks.plane) и проверяют
границы и тексты ошибок. Файл тестов: tests/test_qg0_title_limit.py.
tests:
- id: TC-01
type: unit
description: "Дефолтный лимит 200: заголовок ровно 200 символов -> нет ошибки длины title (PASS на границе)."
module: tests/test_qg0_title_limit.py
setup: "settings.qg0_title_max=200 (дефолт); name='x'*200; description валиден (>=20 символов)."
assert: "В списке _qg0_errors нет элемента про длину title."
covers: [AC-1]
expected: PASS
- id: TC-02
type: unit
description: "Дефолтный лимит 200: заголовок 201 символ -> ошибка длины title, текст упоминает '200'."
module: tests/test_qg0_title_limit.py
setup: "settings.qg0_title_max=200; name='x'*201; description валиден."
assert: "В _qg0_errors есть ошибка длины title и её текст содержит подстроку '200'."
covers: [AC-1]
expected: PASS
- id: TC-03
type: unit
description: "Настраиваемый лимит 120: заголовок 120 символов -> нет ошибки длины title."
module: tests/test_qg0_title_limit.py
setup: "monkeypatch settings.qg0_title_max=120; name='x'*120; description валиден."
assert: "Нет ошибки длины title."
covers: [AC-2]
expected: PASS
- id: TC-04
type: unit
description: "Настраиваемый лимит 120: заголовок 121 символ -> ошибка длины title, текст упоминает '120'."
module: tests/test_qg0_title_limit.py
setup: "monkeypatch settings.qg0_title_max=120; name='x'*121; description валиден."
assert: "Есть ошибка длины title и её текст содержит подстроку '120' (и НЕ '80')."
covers: [AC-2]
expected: PASS
- id: TC-05
type: unit
description: "Graceful: невалидное (нечисловое) значение env ORCH_QG0_TITLE_MAX не роняет инстанцирование Settings и даёт дефолт 200."
module: tests/test_qg0_title_limit.py
setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX','abc'); создать новый экземпляр Settings()."
assert: "Settings() не выбрасывает исключение; settings.qg0_title_max == 200."
covers: [AC-3]
expected: PASS
- id: TC-06
type: unit
description: "Graceful: пустая строка env ORCH_QG0_TITLE_MAX -> дефолт 200, без исключения."
module: tests/test_qg0_title_limit.py
setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX',''); создать новый экземпляр Settings()."
assert: "Settings() не падает; settings.qg0_title_max == 200."
covers: [AC-3]
expected: PASS
- id: TC-07
type: unit
description: "Корректное числовое env -> применяется заданное значение (sanity положительного пути)."
module: tests/test_qg0_title_limit.py
setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX','150'); создать новый экземпляр Settings()."
assert: "settings.qg0_title_max == 150."
covers: [AC-2, AC-3]
expected: PASS
- id: TC-08
type: unit
description: "Нижний лимит title не сломан: заголовок < 5 символов -> ошибка 'Title слишком короткий' при любом верхнем лимите."
module: tests/test_qg0_title_limit.py
setup: "settings.qg0_title_max=200; name='abc' (3 символа); description валиден."
assert: "В _qg0_errors есть ошибка короткого title."
covers: [AC-4]
expected: PASS
- id: TC-09
type: unit
description: "Лимит description не сломан: description < 20 символов -> ошибка 'Description слишком короткий'."
module: tests/test_qg0_title_limit.py
setup: "settings.qg0_title_max=200; name валиден (>=5, <=200); description='short'."
assert: "В _qg0_errors есть ошибка короткого description."
covers: [AC-4]
expected: PASS
- id: TC-10
type: unit
description: "Обратная совместимость: заголовок длиной 81-200 (ранее отклонялся лимитом 80) теперь проходит при дефолте."
module: tests/test_qg0_title_limit.py
setup: "settings.qg0_title_max=200; name='x'*100; description валиден."
assert: "Нет ошибки длины title (раньше при лимите 80 была бы)."
covers: [AC-7]
expected: PASS
- id: TC-11
type: unit
description: "Полный набор тестов зелёный (регрессия не внесена)."
module: tests/
command: "pytest tests/ -q"
assert: "Все тесты проходят."
covers: [AC-5]
expected: PASS
notes:
- "settings импортирован в src.webhooks.plane как 'from ..config import settings', _qg0_errors читает settings.qg0_title_max динамически -> monkeypatch на src.config.settings.qg0_title_max (или импортируемом объекте) меняет поведение в рамках теста."
- "Для TC-05/06/07 нужен СВЕЖИЙ экземпляр Settings(): глобальный src.config.settings создаётся один раз на импорт, поэтому env-тесты инстанцируют Settings() локально, а не полагаются на готовый синглтон."
- "Тесты не требуют сети, БД, агентов или FastAPI TestClient — чистая проверка leaf-функции _qg0_errors и парсинга Settings."

View File

@@ -0,0 +1,143 @@
# ADR-001: Конфигурируемый QG-0 title-лимит с graceful-деградацией env
## Статус
Accepted
## Контекст
QG-0 — inline-валидация входа конвейера (`_qg0_errors` в `src/webhooks/plane.py`),
вызывается из `start_pipeline` (hard-блок) и из `handle_work_item_created`
(soft-warning). Верхний лимит длины заголовка захардкожен: `if len(name) > 80`.
BRD/ТЗ (ORCH-069) установили, что лимит 80 — гигиенический, а не структурный:
ниже по течению от него ничего не зависит (slug режется независимо `[:30]`,
`tasks.title TEXT` без ограничения, Telegram/Plane хранят/экранируют сами).
Валидные заголовки 81200 символов отклоняются на входе без бизнес-причины.
Требуется:
1. Вынести лимит в конфигурируемый параметр `ORCH_QG0_TITLE_MAX`, дефолт 200.
2. **Graceful-деградация** (AC-3): пустое/нечисловое значение env → дефолт 200
**без падения процесса**. Это и есть единственное нетривиальное архитектурное
решение задачи: `pydantic_settings` v2 по умолчанию при непарсящемся в `int`
значении env бросает `ValidationError` на инстанцировании `Settings()`
т.е. краш на старте контейнера (`settings = Settings()` на module-import,
`src/config.py:352`). Для self-hosting это означало бы падение прод-инструмента
из-за опечатки в env — недопустимо.
Стек подтверждён: `pydantic==2.13.4`, `pydantic-settings==2.5.0` (v2 API).
## Решение
### Р-1. Новый параметр Settings
В `src/config.py`, в класс `Settings`, добавить поле (отдельный блок с
комментарием, рядом с прочими `ORCH_*`):
```python
# ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors).
# 80-char cap was a hygiene limit, not structural. Env ORCH_QG0_TITLE_MAX;
# default 200 (was hardcoded 80). Invalid/empty -> default (graceful, no crash).
qg0_title_max: int = 200
```
Env-имя выводится автоматически из `env_prefix = "ORCH_"``ORCH_QG0_TITLE_MAX`.
### Р-2. Механизм graceful-деградации — `field_validator(mode="before")`
Выбран **pydantic v2 `field_validator` с `mode="before"`** как
минимально-инвазивный, локальный для одного поля механизм. Валидатор перехватывает
сырое значение env ДО стандартного `int`-парсинга и при невалидном/пустом входе
возвращает дефолт `200`, гася `ValidationError`:
```python
from pydantic import field_validator
@field_validator("qg0_title_max", mode="before")
@classmethod
def _qg0_title_max_default(cls, v):
# Graceful (ORCH-069 AC-3): empty / non-numeric env -> default 200,
# process must not crash on startup. Never raises.
try:
if v is None or (isinstance(v, str) and v.strip() == ""):
return 200
return int(v)
except (TypeError, ValueError):
return 200
```
Семантика:
- переменная не задана → pydantic не вызывает validator с env, берётся дефолт поля
`200` (стандартное поведение «из коробки»);
- `""`, `"abc"`, мусор → validator возвращает `200`, исключения нет;
- `"120"``int("120") == 120`.
**Почему именно так (рассмотренные альтернативы):**
- *`Optional[int] + None-fallback на месте чтения`* — отвергнуто: размазывает
дефолт по call-site'ам, легко забыть, тип поля перестаёт быть «честным `int`».
- *try/except вокруг `Settings()` на module-level* — отвергнуто: глушит ВСЕ
ошибки конфигурации (маскирует реальные проблемы других полей), слишком грубо.
- *кастомный тип / `Annotated`-валидатор* — избыточно для одного поля.
- `field_validator(mode="before")` локален, не трогает остальные поля, не меняет
публичный тип `int`, тестируется напрямую через `Settings(qg0_title_max=...)` и
env-патч. Контракт «never-raise» консистентен с общим стилем кодовой базы
(`_qg0_errors`, парсеры — defensive).
### Р-3. Использование лимита в `_qg0_errors`
Хардкод `> 80` → динамическое чтение `settings.qg0_title_max` **на каждый вызов**
(чтобы тест мог патчить `settings`), текст ошибки — f-string с актуальным числом:
```python
if len(name) > settings.qg0_title_max:
errors.append(
f"Title слишком длинный (максимум {settings.qg0_title_max} символов)"
)
```
`settings` уже импортирован в `plane.py`. Сигнатура `_qg0_errors(name, description)
-> list` не меняется. Нижние лимиты (`< 5` title, `< 20` description) — без правок.
Граница (ТЗ §4): fail строго при `len(name) > limit``len == limit` PASS,
`limit + 1` FAIL.
### Р-4. Что НЕ меняется (инварианты)
- `STAGE_TRANSITIONS`, `QG_CHECKS` — QG-0 не зарегистрированный stage-gate, а
inline-валидация; реестры не трогаются.
- Схема БД (`tasks.title TEXT`), API, контракты `handle_*`, slug-логика `[:30]`,
soft-QG-0 поведение (общая функция `_qg0_errors`, отдельной правки не требует).
- Топология/инфраструктура (`07-infra-requirements.md`**N/A**) и схема данных
(`08-data-requirements.md`**N/A**) не затрагиваются.
## Последствия
### Плюсы
- Лимит операционно настраивается через env без правки кода и редеплоя кода.
- Чисто аддитивно и обратносовместимо: дефолт 200 > прежних 80 → все ранее
проходившие заголовки проходят (AC-7).
- Опечатка в `ORCH_QG0_TITLE_MAX` не роняет прод-процесс (критично для
self-hosting): graceful-fallback на 200.
- Изменение изолировано в одной функции + одном поле config + одном валидаторе.
### Минусы / ограничения
- Невалидное env «тихо» проглатывается → оператор не сразу заметит опечатку
(лимит молча станет 200). Принято как осознанный trade-off: устойчивость
процесса важнее громкости (consistency с требованием AC-3). Рекомендация:
при желании усилить наблюдаемость — `logger.warning` в validator; **не вводим**
по умолчанию, т.к. на этапе валидации settings логгер может быть не сконфигурён,
и это вне объёма ORCH-069 (можно отдельной QoL-задачей).
- Дефолт 200 — тоже эвристика; структурного верхнего предела по-прежнему нет
(его и не требуется — БД/slug/UI к длине устойчивы).
### Влияние на self-hosting
Прод-контейнер `orchestrator` **не рестартить** в рамках задачи. Изменение
прокатывается штатно через обязательный `deploy-staging`-гейт (8501) перед
прод-деплоем. Риск отказа на старте после деплоя снят самим механизмом Р-2
(graceful), что дополнительно снижает self-hosting-риск.
### Тестируемость (вход для стадий development/testing)
- `_qg0_errors`: патч `settings.qg0_title_max` → проверка границ 200/201 (AC-1),
120/121 (AC-2), нижних лимитов (AC-4).
- validator: `Settings(qg0_title_max="abc")` / `=""` / env-патч → значение 200,
без исключения (AC-3).
## Ссылки
- BRD: `docs/work-items/ORCH-069/01-brd.md`
- ТЗ: `docs/work-items/ORCH-069/02-trz.md`
- Acceptance: `docs/work-items/ORCH-069/03-acceptance-criteria.md`
- Тех-риски: `docs/work-items/ORCH-069/10-tech-risks.md`
</content>
</invoke>

View File

@@ -0,0 +1,21 @@
# Технические риски — ORCH-069
Work Item ID: ORCH-069
Уровень общего риска: **низкий** (аддитивное, обратносовместимое, изолированное изменение).
| # | Риск | Вероятность | Влияние | Митигация |
|---|------|-------------|---------|-----------|
| R-1 | `ValidationError` на старте при мусоре в `ORCH_QG0_TITLE_MAX` → краш прод-процесса (self-hosting) | Средняя (опечатка в env) | Высокое (падение инструмента всех проектов) | `field_validator(mode="before")` гасит невалидный вход → дефолт 200 (ADR Р-2, AC-3). never-raise. |
| R-2 | Чтение лимита один раз на module-import вместо per-call → тесты не смогут патчить settings | Низкая | Среднее (нетестируемость AC-2) | `_qg0_errors` читает `settings.qg0_title_max` динамически на каждый вызов (ADR Р-3). |
| R-3 | Off-by-one на границе (`>=` вместо `>`) | Низкая | Низкое (1 символ) | Явная семантика `len > limit` зафиксирована (ТЗ §4, AC-1/AC-2); тесты на 200/201, 120/121. |
| R-4 | Регресс нижних лимитов (`< 5` title, `< 20` description) при правке функции | Низкая | Среднее | Трогать только верхний лимит; AC-4 покрывает нижние; диф минимален. |
| R-5 | Тихое проглатывание невалидного env → оператор не заметит опечатку | Средняя | Низкое (лимит молча = 200, конвейер работает) | Осознанный trade-off (ADR «Минусы»): устойчивость > громкость. Опц. `logger.warning` — вне объёма. |
| R-6 | Случайное затрагивание вне-объёмных элементов (slug `[:30]`, БД, реестры, `handle_*`, soft-QG-0) | Низкая | Среднее | AC-8 — изоляция; reviewer проверяет диф; ADR Р-4 фиксирует инварианты. |
| R-7 | Документация не обновлена в том же PR (`.env.example`, `.env.staging.example`, `CHANGELOG.md`) | Средняя | Среднее (reviewer REQUEST_CHANGES) | AC-6 чек-лист; документация = golden source (правило 2 CLAUDE.md). |
## Не-риски (явно)
- Схема БД — не меняется (`tasks.title TEXT` без ограничения).
- API/эндпоинты — не меняются.
- Топология/контейнеры/порты — не меняются.
- Откат/миграция — не требуется (дефолт 200 > 80, чисто аддитивно).
</content>

View File

@@ -0,0 +1,68 @@
---
type: review
work_item_id: ORCH-069
verdict: APPROVED
version: 3
---
# Review ORCH-069
## Summary
Реализация конфигурируемого QG-0 title-лимита `ORCH_QG0_TITLE_MAX` (дефолт 200)
выполнена **дословно по ТЗ/ADR** и качественно. Поле `Settings.qg0_title_max`,
graceful `field_validator(mode="before")` (never-raise → дефолт 200), динамическое
чтение `settings.qg0_title_max` в `_qg0_errors` с f-string-текстом ошибки. Код
изолирован (затронуты только `src/config.py` и `src/webhooks/plane.py`), инварианты
не нарушены, нижние лимиты сохранены. Свежий полный прогон на текущем состоянии
ветки: `pytest tests/ -q`**863 passed** (включая 10 новых тестов ORCH-069,
файл `tests/test_qg0_title_limit.py`, все зелёные). Документация обновлена в том же
PR полностью. Блокирующих и must-fix findings нет → **APPROVED**.
## Соответствие ТЗ / ADR
- `src/config.py` — поле `qg0_title_max: int = 200` + валидатор `_qg0_title_max_default`
(`mode="before"`, try/except → 200 при `None`/пустой/нечисловой): 1:1 с ADR Р-1/Р-2
и ТЗ §2/§5. ✓
- `src/webhooks/plane.py` — хардкод `> 80` заменён на `> settings.qg0_title_max`,
текст ошибки динамический (f-string с актуальным числом); сигнатура `_qg0_errors`,
нижний лимит title `< 5`, проверка description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓
- Граница строгая (`len == limit` PASS, `limit+1` FAIL) — подтверждена tc01tc04. ✓
- Инварианты (ADR Р-4 / AC-8): `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, slug `[:30]`,
soft-QG-0, API — НЕ изменены (diff `src/` = только 2 файла). ✓
## Acceptance criteria
- AC-1 (дефолт 200, граница 201, текст упоминает 200) — tc01/tc02 ✓
- AC-2 (лимит 120, граница 121, текст 120 не 80) — tc03/tc04 ✓
- AC-3 (graceful пустое/`abc` → 200 без краха) — tc05/tc06 + позитив tc07 + валидатор ✓
- AC-4 (нижние лимиты title<5 / desc<20) — tc08/tc09 ✓
- AC-5 (pytest зелёный) — 863 passed ✓
- AC-6 (документация в том же PR) — выполнен полностью ✓
- AC-7 (обратная совместимость, ≤80 проходит при 200) — tc10 ✓
- AC-8 (изоляция изменений) — ✓
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice-to-have (не блокирует)
- В конце `06-adr/ADR-001-configurable-qg0-title-limit.md` присутствуют артефактные
хвостовые теги (`</content>`, `</invoke>`). Косметика в артефакте стадии architecture;
на корректность кода/контракта не влияет. Править артефакт чужой стадии в рамках
ревью не уполномочен — отмечено для будущей чистки.
## Документация
- `.env.example` — добавлен `ORCH_QG0_TITLE_MAX=200` с комментарием. ✓
- `.env.staging.example` — добавлен `ORCH_QG0_TITLE_MAX=200`. ✓
- `CHANGELOG.md` — подробная запись об ORCH-069 (раздел Added). ✓
- `README.md` — таблица env-конфигов дополнена строкой `ORCH_QG0_TITLE_MAX`. ✓
- ADR `06-adr/ADR-001-configurable-qg0-title-limit.md` — присутствует, согласован
с кодом. ✓
- `docs/architecture/README.md` / `CLAUDE.md` — обновления не требуют (QG-0 — inline
soft/hard-валидация входа, не зарегистрированный stage-gate; API/стадии/QG-реестр
не менялись). ОК.

View File

@@ -0,0 +1,98 @@
---
type: test-report
work_item_id: ORCH-069
result: PASS
---
# Test Report — ORCH-069
QG-0 title-лимит → параметр `ORCH_QG0_TITLE_MAX` (дефолт 200)
## Окружение
- Python: 3.12.13
- pytest: 8.3.3 (plugins: anyio-4.13.0, asyncio-0.23.8; asyncio mode=auto)
- Ветка: `feature/ORCH-069-qg-0-title-orch-qg0-title-max-`
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-069-qg-0-title-orch-qg0-title-max-`
- Prod-health (8500): `{"status":"ok","service":"orchestrator"}` — не трогался (self-hosting safety)
- Дата: 2026-06-08
## Предусловия
- Review-вердикт `12-review.md`: **APPROVED** (version 3) ✓
- Изменения изолированы: `src/config.py`, `src/webhooks/plane.py` (+ тесты, + документация)
## Результаты по тест-плану (04-test-plan.yaml)
| TC ID | Описание | Покрывает | Результат |
|-------|----------|-----------|-----------|
| TC-01 | Дефолт 200: title=200 → нет ошибки длины (граница PASS) | AC-1 | PASS |
| TC-02 | Дефолт 200: title=201 → ошибка длины, текст упоминает «200» | AC-1 | PASS |
| TC-03 | Лимит 120: title=120 → нет ошибки длины | AC-2 | PASS |
| TC-04 | Лимит 120: title=121 → ошибка, текст «120» (не «80») | AC-2 | PASS |
| TC-05 | Graceful: env `abc` → дефолт 200, без краха `Settings()` | AC-3 | PASS |
| TC-06 | Graceful: пустой env `""` → дефолт 200, без исключения | AC-3 | PASS |
| TC-07 | Валидный env `150` → применяется 150 (позитивный путь) | AC-2, AC-3 | PASS |
| TC-08 | Нижний лимит title < 5 не сломан | AC-4 | PASS |
| TC-09 | Лимит description < 20 не сломан | AC-4 | PASS |
| TC-10 | Обратная совместимость: title 81200 проходит при дефолте | AC-7 | PASS |
| TC-11 | Полный набор тестов зелёный (нет регрессии) | AC-5 | PASS |
## Сопоставление с критериями приёмки (03-acceptance-criteria.md)
| AC | Критерий | Статус |
|----|----------|--------|
| AC-1 | Дефолт 200, граница на 201, текст упоминает 200 | PASS (TC-01/02) |
| AC-2 | Настраиваемый лимит 120, граница 121, текст 120 | PASS (TC-03/04/07) |
| AC-3 | Graceful при пустом/нечисловом значении → 200 | PASS (TC-05/06) |
| AC-4 | Нижние лимиты title<5 / description<20 не сломаны | PASS (TC-08/09) |
| AC-5 | Юнит-тесты зелёные (весь набор) | PASS (863 passed) |
| AC-6 | Документация в том же PR (.env.example, .env.staging.example, CHANGELOG, README) | PASS (подтверждено review) |
| AC-7 | Обратная совместимость (≤80 проходит при 200) | PASS (TC-10) |
| AC-8 | Изоляция: slug `[:30]`, БД, STAGE_TRANSITIONS/QG_CHECKS, handle_* не тронуты | PASS (diff = 2 файла src/) |
## Smoke test API (prod 8500, read-only)
- `GET /health``{"status":"ok","service":"orchestrator"}` — OK
- `GET /status` → отдаёт активные задачи (ORCH-069 в стадии `testing`) — OK
- `GET /queue``counts: queued=0 running=1 done=459 failed=4 cancelled=1`; breaker `closed`, preflight ok — OK
## Целевой прогон ORCH-069 (tests/test_qg0_title_limit.py)
```
collected 10 items
tests/test_qg0_title_limit.py::test_tc01_default_limit_200_boundary_pass PASSED
tests/test_qg0_title_limit.py::test_tc02_default_limit_200_boundary_fail PASSED
tests/test_qg0_title_limit.py::test_tc03_custom_limit_120_boundary_pass PASSED
tests/test_qg0_title_limit.py::test_tc04_custom_limit_120_boundary_fail PASSED
tests/test_qg0_title_limit.py::test_tc05_graceful_non_numeric_env PASSED
tests/test_qg0_title_limit.py::test_tc06_graceful_empty_env PASSED
tests/test_qg0_title_limit.py::test_tc07_valid_numeric_env PASSED
tests/test_qg0_title_limit.py::test_tc08_short_title_still_errors PASSED
tests/test_qg0_title_limit.py::test_tc09_short_description_still_errors PASSED
tests/test_qg0_title_limit.py::test_tc10_backward_compat_titles_81_to_200 PASSED
======================== 10 passed, 1 warning in 0.31s =========================
```
## Полный прогон (pytest tests/ -q)
```
........................................................................ [ 8%]
........................................................................ [ 16%]
........................................................................ [ 25%]
........................................................................ [ 33%]
........................................................................ [ 41%]
........................................................................ [ 50%]
........................................................................ [ 58%]
........................................................................ [ 66%]
........................................................................ [ 75%]
........................................................................ [ 83%]
........................................................................ [ 91%]
....................................................................... [100%]
863 passed, 1 warning in 21.49s
```
(Единственный warning — PydanticDeprecatedSince20 в `src/config.py:5`, существующий
class-based config; к ORCH-069 не относится, не является ошибкой.)
## Итог
**PASS** — все 11 TC из тест-плана пройдены, все 8 критериев приёмки выполнены,
полный регресс зелёный (863 passed), smoke-тесты API OK. Регрессии не внесены.
Задача готова к переходу на стадию `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-069
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,25 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T11:20:02+00:00
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed via `docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub` (run through the Docker Engine API since the `docker` CLI is not installed on the host; equivalent in-container execution per ORCH-048/ADR-001).
**Result: 8/10 checks PASS — exit code 0 → SUCCESS.**
All REAL (pipeline) checks are green. The two failing checks are the known SANDBOX_INFRA checks (C9a/C9b), waived per ORCH-061 because every REAL check passed.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
## Block results
- **[Block A] SMOKE** — A1 /health 200 ok ✓ · A2 /queue 200 with counts ✓ · A3 ORCH_STAGING=true ✓
- **[Block B] ACCESS** — B4 Plane sandbox accessible ✓ · B5 Gitea orchestrator-sandbox push=true ✓ · B6 Registry isolation (sandbox present, prod ET/ORCH absent) ✓
- **[Block C] E2E (stub)** — C7 Create issue in Plane SANDBOX ✓ · C8 Trigger pipeline via /webhook/plane ✓ · C9a Branch appears in orchestrator-sandbox ✗ (SANDBOX_INFRA, waived) · C9b Analyst job enqueued ✗ (SANDBOX_INFRA, waived)
REAL failed: none. SANDBOX_INFRA failed: C9a, C9b (waived). tolerance: staging_infra_tolerance_enabled=True.

View File

@@ -0,0 +1,25 @@
---
security_status: PASS
secrets_found: 0
deps_blocking: 0
deps_warning: 4
deps_audit_degraded: false
---
# Security Report — ORCH-069
Детерминированный security-гейт (ORCH-022): secret-scanning (gitleaks, offline) + dependency audit (pip-audit). Машинный вердикт читается ТОЛЬКО из frontmatter выше.
## Verdict
clean: 0 secrets, 0 blocking CVE(s)
## Secrets
- None
## Dependencies (blocking)
- None
## Dependencies (warning)
- `pytest==8.3.3` — GHSA-6w46-j5rx-g56g severity=UNKNOWN fix=9.0.3
- `starlette==0.38.6` — PYSEC-2026-161 severity=UNKNOWN fix=1.0.1
- `starlette==0.38.6` — GHSA-f96h-pmfr-66vw severity=UNKNOWN fix=0.40.0
- `starlette==0.38.6` — GHSA-2c2j-9gv5-cj73 severity=UNKNOWN fix=0.47.2

View File

@@ -0,0 +1,7 @@
# Business Request: CRIT BUG: фантомный merge — деплой без слияния в main
Work Item ID: ORCH-071
## Description
TBD

View File

@@ -0,0 +1,53 @@
# BRD — ORCH-071: Фантомный merge — деплой без слияния в main
## 1. Контекст и тип
- **Тип:** BUG CRITICAL (целостность `main` / надёжность деплоя, self-hosting).
- **Обнаружено:** Слава + Стрим, 2026-06-08, при разборе «ORCH-067 не подхватился».
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`.
- **Подозрение на регресс:** ORCH-065 (idempotent merge / lease-reclaim) — последний честный merge (PR#66).
- **Связано:** восстановление текущего `main` ведётся ОТДЕЛЬНО (ветка `integ/restore-main-2026-06-08`); эта задача — ROOT-FIX, чтобы фантом не повторялся.
## 2. Проблема (бизнес-формулировка)
Self-deploy (Phase B) для self-hosting репо `orchestrator` собирает прод-образ из ВЕТКИ задачи и рапортует `finalize SUCCESS` + post-deploy `HEALTHY`, **но git-merge ветки в `main` НЕ происходит**. PR остаётся `open`. Следующая задача срезает свою ветку от устаревшего `main` → теряет код незалитых предшественников.
Накопительно потеряны в `main`: **ORCH-022, 059, 066, 068** (PR#67/68/69/70 — open). Последний реально слитый — ORCH-065 (PR#66).
## 3. Подтверждённый root cause (по результатам код-аудита)
Гипотеза A постмортема подтверждена аудитом кода ветки:
1. **В `src/` НЕТ кода, выполняющего merge PR в `main`** (`grep` по `pulls/.../merge`, `/merge`, `merge_pr` — 0 совпадений). Фактический merge выполняет ТОЛЬКО LLM-агент `deployer` через Bash в начале стадии `deploy` (см. `.openclaw/agents/deployer.md`).
2. Для self-hosting (`orchestrator`) стадия `deploy` оркеструется **детерминированным кодом** (`stage_engine._handle_self_deploy_phase_b``self_deploy.initiate_deploy` → finalizer `run_deploy_finalizer`), и агент `deployer` **НЕ запускается** (так предписывает `deployer.md`). Detached host-процесс делает retag staging-образа на прод-тег + рестарт 8500. **Ни одна фаза A/B/C не вызывает merge ветки в `main`.**
3. `run_deploy_finalizer` маппит exit-code хука `0→SUCCESS`, пишет `14-deploy-log.md` и вызывает `advance_stage(..., finished_agent="deployer")`. Гейт `check_deploy_status` читает только `deploy_status:` из артефакта → `SUCCESS → done`. **Состояние `main` нигде не верифицируется.**
Итог: для self-hosting путь `deploy` структурно НЕ содержит шага merge-в-main, а `done` достигается исключительно по deploy-маркеру. «Зелёный» деплой + здоровый прод (образ из рабочей ветки) маскируют отсутствие merge — сигнала о проблеме нет, пока следующая задача не потеряет код предшественника.
Вторичный фактор (усиливает риск даже если merge добавить наивно): Phase B **рестартит прод-контейнер**, поэтому любой держатель merge-lease / незавершённый git-шаг внутри процесса умирает до завершения merge (урок №3 постмортема).
## 4. Бизнес-цели
| ID | Цель |
|----|------|
| **G1** | Деплой ВЕРИФИЦИРУЕТ, что задеплоенный commit реально влит в `main` ПОСЛЕ деплоя (deployed SHA — предок `origin/main` ИЛИ `PR.merged==true`). Иначе — alert, задача НЕ `done`. |
| **G2** | Задача → `done` ТОЛЬКО при подтверждённом merge (`PR.merged==true`); маркеров `finalize`/`post-deploy` недостаточно. |
| **G3** | Merge в `main` завершается и подтверждается ДО рестарта прод-контейнера, ЛИБО merge вынесен в шаг, переживающий рестарт (паттерн `requeue_running_jobs` для merge-в-main). |
| **G4** | Диагностический runbook (4 проверки из постмортема) — в `docs/operations`. |
## 5. Не-цели
- Не менять source-of-truth (Plane), схему БД.
- Не отменять self-hosting safety (no auto-rollback / no-restart-others) — наоборот, усилить верификацией.
- Восстановление текущего `main` (долив 022/059/066/068) — ОТДЕЛЬНАЯ ветка `integ/restore-main-2026-06-08`, вне scope.
## 6. Инварианты (обязательны к соблюдению)
| ID | Инвариант |
|----|-----------|
| **INV-1** | **never-raise** на шаге верификации — при ошибке шлётся alert, не падение процесса/конвейера. |
| **INV-2** | self-hosting safety: верификация НЕ рестартит и НЕ роняет прод-контейнер `orchestrator` (8500), не трогает другие проекты. |
| **INV-3** | Ручной approve прод-деплоя (триггер «Confirm Deploy», ORCH-059) сохранён — новая логика не вводит авто-деплой. |
| **INV-4** | Никогда не делать force-push / прямой push в `main`; merge только через PR-merge API Gitea (как у deployer-агента сегодня). |
| **INV-5** | Идемпотентность: повторный прогон (re-drive/reaper/двойной webhook) не делает второй merge и не ломает контракты (опора на `pr_already_merged`, ORCH-065). |
## 7. Заинтересованные стороны
- **Owner** — одобряет прод-деплой («Confirm Deploy»), получает alert при «deployed but not merged».
- **Все проекты на инстансе** (enduro-trails) — косвенно: целостность `main` орка влияет на инструмент, обслуживающий их из общей БД/очереди.
## 8. Критерий успеха (бизнес-уровень)
После доработки невозможно состояние «задача `done` + прод задеплоен, а PR `open` / commit не в `main`»: либо merge подтверждён и задача `done`, либо задача НЕ `done` и поднят alert «deploy succeeded but not merged». Воспроизведение исходного сценария на staging показывает, что `main` реально получает commit.

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