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>
This commit is contained in:
2026-06-08 19:06:22 +03:00
committed by stream
parent 9019e12d98
commit a74379f657
24 changed files with 1686 additions and 2 deletions

View File

@@ -380,6 +380,22 @@ def render_task_tracker(task_id: int) -> str:
status_line = f"\U0001f4cd {status_label}"
lines = [header, status_line, bar]
# ORCH-026 (B-4): waiting-line for a task blocked by an unfinished declared
# dependency. Shows WHAT the task is waiting on ("⏳ ждёт ORCH-NNN"),
# so the single tracker card (invariant preserved) makes the wait visible.
# Never breaks the render: any error -> no waiting-line.
if not done:
try:
from . import task_deps
from .config import settings as _settings
if getattr(_settings, "task_deps_enabled", False):
ready, waiting_on = task_deps.is_task_ready(task_id)
if not ready and waiting_on:
waits = ", ".join(link_for(w) for w in waiting_on)
lines.append(f"⏳ ждёт {waits}")
except Exception:
pass
def _stage_line(label, run):
usage = {
"input_tokens": run["input_tokens"],