Compare commits

..

1 Commits

Author SHA1 Message Date
Dev
b6d6b53f39 ci: add Gitea Actions workflow (pytest) for orchestrator self-hosting
Some checks failed
CI / test (push) Failing after 18s
CI / test (pull_request) Failing after 8s
2026-06-04 21:07:02 +03:00
951 changed files with 496 additions and 109461 deletions

View File

@@ -1,8 +1,4 @@
ORCH_PLANE_API_URL=http://plane-app-api-1:8000
# External (browser) web URL of Plane for clickable issue links in notifications
# (ORCH-017). Falls back to ORCH_PLANE_API_URL; a loopback fallback is treated as
# "no web URL" and the Plane link is omitted. Example: https://plane.example.org
ORCH_PLANE_WEB_URL=
ORCH_PLANE_API_TOKEN=
ORCH_PLANE_WORKSPACE_SLUG=
ORCH_PLANE_WEBHOOK_SECRET=
@@ -12,501 +8,3 @@ ORCH_GITEA_WEBHOOK_SECRET=
ORCH_CLAUDE_BIN=/usr/bin/claude
ORCH_REPOS_DIR=/home/slin/repos
ORCH_DB_PATH=/app/data/orchestrator.db
# ── 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 (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer)
# -> high; developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical
# agents (tester/deployer) -> medium. NB: an empty ORCH_AGENT_EFFORT_*= no longer
# zeroes the effort — the launcher falls back to a per-role floor (= the config.py
# class-default) so each role still runs at its canonical level (ORCH-081).
ORCH_AGENT_EFFORT_DEFAULT=high
ORCH_AGENT_EFFORT_ANALYST=high
ORCH_AGENT_EFFORT_ARCHITECT=high
ORCH_AGENT_EFFORT_DEVELOPER=xhigh
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
# green parallel branches can't break main.
# ENABLED -> global kill-switch (false -> whole gate is a no-op pass).
# REPOS -> CSV of repos where the gate is REAL; empty -> only the self-hosting
# repo (orchestrator); other repos -> conditional no-op (mirrors ORCH-35).
# RETEST_TIMEOUT_S -> wall-clock budget for the post-rebase re-test.
# RETEST_TARGET -> pytest target for the re-test.
# LOCK_TIMEOUT_S -> max merge-lease age before a stale lease is reclaimed.
# DEFER_DELAY_S -> delay before re-running the gate when the lock is busy.
# DEFER_MAX_ATTEMPTS -> defer retries before escalation (avoids livelock).
ORCH_MERGE_GATE_ENABLED=true
ORCH_MERGE_GATE_REPOS=
ORCH_MERGE_RETEST_TIMEOUT_S=600
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-088 (Stage 1, serial e2e): per-repo serial gate. A NEW task's analyst-job does
# NOT enter analysis (no branch cut, no analyst) while the same repo has an EARLIER
# unfinished task (FIFO, tasks.id < the job's task) OR the repo is frozen. The branch
# cut is DEFERRED from start_pipeline to the analyst-job claim so its base is a fresh
# origin/main already containing the predecessor (anti-stale-base). Gate lives in
# claim_next_job (offline hot-path, fail-OPEN on error); freeze (FR-5) is a durable
# repo_freeze row set on post-deploy DEGRADED, cleared manually via
# POST /serial-gate/unfreeze?repo=<repo>. Leaf src/serial_gate.py (never-raise).
# SERIAL_GATE_ENABLED=false -> claim AND start_pipeline are 1:1 as before ORCH-088.
# SERIAL_GATE_REPOS (CSV) -> scope; EMPTY = ALL repos (not self-hosting-only).
# SERIAL_GATE_FREEZE_ENABLED=false -> the rollback-freeze layer is off (not set/read).
ORCH_SERIAL_GATE_ENABLED=true
ORCH_SERIAL_GATE_REPOS=
ORCH_SERIAL_GATE_FREEZE_ENABLED=true
# ORCH-090: STOP-status task cancellation (stop active agent + full progress reset)
# and the relaunch-hole close. A dedicated Plane "STOP" status (logical key `stop`,
# fail-closed: absent from _DEFAULT_STATES, so a board without the status -> no-op)
# routes to a cancel handler that drives the task to the system-terminal state
# `cancelled` (stop agent via the graceful SIGTERM cascade, cancel all jobs, remove
# worktree + delete the remote feature branch [never main / never force-push],
# tombstone the natural keys for a clean re-create via "To Analyse"; docs preserved).
# STOP during a critical merge/deploy window is DEFERRED until the irreversible step
# finishes honestly. The relaunch-hole gate restricts the "To Analyse" agent relaunch
# to the `analysis` stage (the sole Needs-Input owner). Additive, never-raise.
# Infra precondition: create a "STOP" status with the `cancelled` group on the ORCH
# board (07-infra-requirements.md). Leaf src/cancel.py.
# STOP_STATUS_ENABLED=false -> STOP handling AND the relaunch-hole gate are inert
# (behaviour strictly as before ORCH-090).
# STOP_STATUS_REPOS (CSV) -> scope; EMPTY = ALL repos (cancellation is meaningful
# for enduro too).
ORCH_STOP_STATUS_ENABLED=true
ORCH_STOP_STATUS_REPOS=
# ORCH-019: bug-fast-track — a cheaper/shorter pipeline route for bug-fix tasks.
# A task carrying the Plane `Bug` label skips the whole `architecture` stage; EVERY
# Quality Gate / sub-gate runs UNCHANGED (route is a scheduler property, not a gate).
# Additive, never-raise, fail-safe -> full cycle. Infra precondition: create a `Bug`
# label on the ORCH board (its absence = full cycle, fail-safe). Leaf src/bug_fast_track.py.
# BUG_FAST_TRACK_ENABLED=false -> start_pipeline AND advance_stage are 1:1 as before
# ORCH-019 (zero regression).
# BUG_FAST_TRACK_LABEL -> Plane label that activates the track (default `Bug`).
# BUG_FAST_TRACK_REPOS (CSV) -> scope; EMPTY = self-hosting only (orchestrator).
ORCH_BUG_FAST_TRACK_ENABLED=true
ORCH_BUG_FAST_TRACK_LABEL=Bug
ORCH_BUG_FAST_TRACK_REPOS=
# ORCH-094: terminal-window-aware guard for the three deploy-phase Plane status
# setters (set_issue_awaiting_deploy / set_issue_deploying / set_issue_monitoring).
# A DB stage=done task converges to Done idempotently instead of flapping
# Awaiting <-> Monitoring, EXCEPT the legitimate post-deploy Monitoring while the
# window is active (ARMED & not DONE). Leaf src/deploy_status_guard.py, never-raise;
# STAGE_TRANSITIONS / QG_CHECKS / machine-verdict keys untouched (no DB migration).
# DEPLOY_STATUS_GUARD_ENABLED=false -> setters are terminal-blind (1:1 pre-ORCH-094).
# DEPLOY_STATUS_GUARD_REPOS (CSV) -> scope; EMPTY = self-hosting only (orchestrator),
# the only repo where deploy-phase statuses are set.
ORCH_DEPLOY_STATUS_GUARD_ENABLED=true
ORCH_DEPLOY_STATUS_GUARD_REPOS=
# 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.
# MERGE_VERIFY_AUTOCREATE_PR_ENABLED -> ORCH-082: guarantee an open code-PR
# (head==branch, base==main) via merge_gate.ensure_open_pr
# BEFORE the deterministic merge_pr (fixes the false HOLD
# "no open PR"). false -> exactly pre-ORCH-082 behaviour.
# Reuses the merge-verify scope; non-self repos -> no-op.
ORCH_MERGE_VERIFY_ENABLED=true
ORCH_MERGE_VERIFY_REPOS=
ORCH_MERGE_PR_TIMEOUT_S=60
ORCH_MERGE_VERIFY_TIMEOUT_S=60
ORCH_REGRESSION_GUARD_ENABLED=true
ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=true
# ORCH-093: deterministic merge-actor retry of TRANSIENT Gitea merge errors. merge_pr
# wraps ONLY the mutating POST /pulls/{n}/merge in a bounded exponential-backoff
# retry-loop on transient outcomes (405 "try again later" / 408 / 5xx / network /
# timeout, and 409|422 while the PR is still mergeable); terminal outcomes
# (403/404/real conflict) -> fast honest False (the ORCH-071/081 HOLD backstop is
# unchanged). Fixes the ORCH-063 false HOLD + manual re-merge. The already-in-main
# guard (no commits beyond origin/main -> no garbage PR) is always-on under
# MERGE_VERIFY_AUTOCREATE_PR_ENABLED (no separate flag).
# MERGE_RETRY_ENABLED -> kill-switch; false -> exactly one POST (one-shot, prior behaviour).
# MERGE_RETRY_MAX_ATTEMPTS -> max POST attempts on a transient outcome.
# MERGE_RETRY_BACKOFF_BASE_S -> exponential backoff base seconds (sleep = base*2^(i-1)).
# MERGE_RETRY_BACKOFF_MAX_S -> per-sleep backoff ceiling seconds (bounds total wait).
ORCH_MERGE_RETRY_ENABLED=true
ORCH_MERGE_RETRY_MAX_ATTEMPTS=3
ORCH_MERGE_RETRY_BACKOFF_BASE_S=2
ORCH_MERGE_RETRY_BACKOFF_MAX_S=5
# 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
# deterministic phases (A: request approve, B: human Approved -> detached deploy,
# C: finalizer maps hook exit-code -> deploy_status). Non-self repos: unchanged
# synchronous ssh deploy. SECRETS / host paths live ONLY on the host — do NOT commit.
# SELF_DEPLOY_ENABLED -> global kill-switch (false -> legacy synchronous deploy for all).
# SELF_DEPLOY_REPOS -> CSV of repos where Phase A/B/C is REAL; empty -> only the
# self-hosting repo (orchestrator); others -> no-op (mirrors ORCH-35).
# DEPLOY_REQUIRE_MANUAL_APPROVE -> require a human Plane "Approved" before the prod
# deploy (true on rollout; full auto is ORCH-54).
# DEPLOY_FINALIZE_DELAY_S -> delay before the first/each finalize poll (>= hook+health).
# DEPLOY_FINALIZE_MAX_ATTEMPTS -> bounded finalize-defer budget (anti-livelock).
# DEPLOY_SSH_USER / DEPLOY_SSH_HOST -> ssh target for the host hook (DEPLOY_SSH_HOST
# empty -> detached deploy will NOT launch; set on the host).
# DEPLOY_HOOK_SCRIPT -> path to the hook ON THE HOST (relative to the repo).
# DEPLOY_HOST_REPO_PATH -> orchestrator clone path on the host.
# DEPLOY_PROD_SOURCE_IMAGE -> staging-validated image, retagged build-once (no rebuild).
# DEPLOY_PROD_TARGET_SERVICE / _PORT / _IMAGE / _COMPOSE_PROFILE -> prod compose profile.
# DEPLOY_PROD_PREV_IMAGE_FILE -> prod prev-image snapshot (separate from staging's).
ORCH_SELF_DEPLOY_ENABLED=true
ORCH_SELF_DEPLOY_REPOS=
ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE=true
ORCH_DEPLOY_FINALIZE_DELAY_S=90
ORCH_DEPLOY_FINALIZE_MAX_ATTEMPTS=10
ORCH_DEPLOY_SSH_USER=slin
ORCH_DEPLOY_SSH_HOST=
ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh
ORCH_DEPLOY_HOST_REPO_PATH=/home/slin/repos/orchestrator
ORCH_DEPLOY_PROD_SOURCE_IMAGE=orchestrator-orchestrator-staging
ORCH_DEPLOY_PROD_TARGET_SERVICE=orchestrator
ORCH_DEPLOY_PROD_TARGET_PORT=8500
ORCH_DEPLOY_PROD_TARGET_IMAGE=orchestrator-orchestrator
ORCH_DEPLOY_PROD_COMPOSE_PROFILE=
ORCH_DEPLOY_PROD_PREV_IMAGE_FILE=.deploy-prev-image-prod
# ORCH-058: staging-image provenance before the BUILD-ONCE prod retag (INV-FRESH).
# Guarantees the staging image promoted to prod is the EXACT artefact rebuilt from the
# validated commit — two layers, self-hosting only:
# A (liveness): QG sub-check `check_staging_image_fresh` on the deploy-staging->deploy
# edge rebuilds orchestrator-orchestrator-staging from the validated commit + recreates
# 8501; FAIL -> rollback to development. (builds/recreate STAGING only, never prod.)
# B (safety): the Dockerfile stamps `org.opencontainers.image.revision`; the prod hook
# fail-closes (exit 1) before `docker tag` if SOURCE_IMAGE's label != EXPECTED_REVISION.
# ENABLED -> single kill-switch for A+B as a WHOLE (never "B without A"); false -> legacy.
# REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting (orchestrator).
ORCH_IMAGE_FRESHNESS_ENABLED=true
ORCH_IMAGE_FRESHNESS_REPOS=
# ORCH-061: staging-verdict tolerance to sandbox-infra-only FAILs. The self-hosting
# orchestrator looped on deploy-staging because staging_check.py exited 1 on ANY FAIL,
# so two infra-only checks (C9a sandbox branch / C9b analyst-job — caused by SANDBOX
# bot accounts not being members of the sandbox Plane project, NOT a pipeline regress)
# forced staging_status: FAILED -> rollback -> loop. With this ON, C9a/C9b are WAIVED
# to SUCCESS when every REAL check is green; any REAL failure still fails closed.
# true (default) -> tolerant; false -> legacy strict (1:1 pre-ORCH-061, any FAIL rolls back).
# Lives in .env.staging (the staging instance). CLI --strict overrides this per-run.
ORCH_STAGING_INFRA_TOLERANCE_ENABLED=true
# ORCH-053: stuck-task reconciler (sweeper for lost webhooks). A background daemon
# replays a missed stage transition through the SAME gates/handlers a webhook would,
# fixing tasks that got stuck on a dropped event (502 on rebuild, no Plane/Gitea
# retries, unresolved sha->branch).
# ENABLED -> global kill-switch (self-hosting safety / staged rollout).
# PLANE_ENABLED -> separate flag for the F-2 Plane-API poll (mute only F-2).
# INTERVAL_S -> background sweep period (seconds).
# GRACE_DEFAULT_S -> default "stuck" threshold on tasks.updated_at (seconds).
# GRACE_OVERRIDES_JSON -> per-stage thresholds, e.g. {"development":300}; bad JSON -> default.
# NOTIFY_UNBLOCK -> send a Telegram message when a stuck task is unblocked.
# SKIP_BLOCKED_ENABLED -> ORCH-060 F-1 Guard 2: skip reconciling issues a human moved
# to Blocked / Needs Input (per-candidate Plane state lookup).
# false mutes ONLY the networked Guard 2; Guard 1 (escalated by
# developer retries, local+deterministic) is always active.
ORCH_RECONCILE_ENABLED=true
ORCH_RECONCILE_PLANE_ENABLED=true
ORCH_RECONCILE_INTERVAL_S=120
ORCH_RECONCILE_GRACE_DEFAULT_S=600
ORCH_RECONCILE_GRACE_OVERRIDES_JSON=
ORCH_RECONCILE_NOTIFY_UNBLOCK=true
ORCH_RECONCILE_SKIP_BLOCKED_ENABLED=true
# ORCH-068: TTL (seconds) for the per-project Plane states cache (plane_sync
# _STATES_CACHE). Historically the cache lived for the whole process lifetime,
# so a status added to Plane after start was invisible until a restart
# ("stale set -> no pipeline action"). With a TTL the entry self-heals by
# re-fetching /states/ once it expires (reuses reload_project_states()).
# >0 -> re-fetch after this many seconds (default 300 = 5 min);
# 0 -> disable TTL -> strictly the previous lifetime cache (back-compat).
ORCH_PLANE_STATES_TTL_S=300
# ORCH-065: job-reaper + proactive merge-lease reclaim. A background daemon thread
# (src/job_reaper.py, started LAST in main.lifespan after requeue_running_jobs) reaps
# zombie 'running' jobs whose monitor/process died before writing the terminal status
# (one zombie at max_concurrency=1 blocks the whole shared queue) and periodically
# reclaims dead/stale merge-leases. Liveness is three-tier: Tier-1 dead jobs.pid
# (os.kill(pid,0)) after REAPER_DEAD_TICKS consecutive dead ticks (anti-false-positive
# for a live agent); Tier-2 agent_runs.exit_code recorded but job still 'running'
# (only after a REAPER_FINALIZE_GRACE_S finalization grace, so a live monitor still
# doing git push / PR / Plane comments is never reaped); Tier-3 backstop after
# REAPER_MAX_RUNNING_S. The terminal flip carries an atomic status='running' guard and
# precedes any advance/enqueue (claim-before-act) so it never double-processes/-advances
# a row racing a late monitor or requeue_running_jobs.
# REAPER_ENABLED -> global kill-switch (false -> strictly prior behaviour).
# REAPER_INTERVAL_S -> background scan period (seconds).
# REAPER_DEAD_TICKS -> consecutive dead-pid ticks before reaping (Tier-1, >=2).
# REAPER_MAX_RUNNING_S -> Tier-3 backstop ceiling; must exceed max agent_timeout+grace.
# REAPER_FINALIZE_GRACE_S -> Tier-2 grace: how long agent_runs.exit_code must have been
# recorded before a still-'running' job is reaped; MUST exceed
# the max finalization window (git push + PR + Plane comments).
# LEASE_RECLAIM_ENABLED -> kill-switch for the proactive stale/dead lease reclaim
# (false -> only the legacy lazy TTL reclaim in acquire_merge_lease).
# (reuse) ORCH_MERGE_LOCK_TIMEOUT_S -> lease TTL; ORCH_MERGE_GATE_REPOS -> reclaim scope.
ORCH_REAPER_ENABLED=true
ORCH_REAPER_INTERVAL_S=60
ORCH_REAPER_DEAD_TICKS=2
ORCH_REAPER_MAX_RUNNING_S=3600
ORCH_REAPER_FINALIZE_GRACE_S=300
ORCH_LEASE_RECLAIM_ENABLED=true
# ORCH-063: disk-watchdog — background heartbeat that measures HOST-FS fill via the
# mounted bind-paths (/repos, /app/data) with shutil.disk_usage (NOT the container
# overlay /) and Telegram-alerts the operator at >= threshold. On 07.06.2026 the
# mva154 host disk silently hit 100% and stalled the WHOLE self-hosting pipeline;
# this is the missing proactive signal. Daemon thread modelled on reconciler/reaper
# (start/stop in main.lifespan, /queue snapshot, never-raise). Anti-spam state is
# in-memory (no DB migration); the watchdog only READS fill and SENDS Telegram — it
# never touches the disk/container or restarts prod (self-hosting safety).
# DISK_MONITOR_ENABLED -> kill-switch; false -> the daemon does not start (1:1 as before).
# DISK_MONITOR_INTERVAL_S -> heartbeat measurement period, seconds (order of minutes).
# DISK_MONITOR_THRESHOLD_PCT -> fill % that triggers the alert (Owner-fixed 85; valid 1..100).
# DISK_MONITOR_REALERT_S -> cooldown between repeat alerts while above threshold (~6h).
# DISK_MONITOR_PATHS -> CSV of monitored HOST bind-paths; empty -> /repos,/app/data.
ORCH_DISK_MONITOR_ENABLED=true
ORCH_DISK_MONITOR_INTERVAL_S=300
ORCH_DISK_MONITOR_THRESHOLD_PCT=85
ORCH_DISK_MONITOR_REALERT_S=21600
ORCH_DISK_MONITOR_PATHS=/repos,/app/data
# ORCH-062: build-cache-pruner — the "second half" of the disk-watchdog
# (watchdog SIGNALS, pruner CLEANS). A daemon thread modelled on disk_watchdog
# that periodically runs STRICTLY `docker builder prune -f --filter until=<until>`
# on the HOST over ssh (BuildKit GC). Touches ONLY the build cache: never
# images/containers of running services, never restarts the docker daemon or the
# prod container (self-hosting safety). State is in-memory (no DB migration). No
# ssh host configured -> the tick is a no-op. See docs/operations/INFRA.md.
# BUILD_CACHE_PRUNE_ENABLED -> kill-switch; false -> the daemon does not start (1:1 as before).
# BUILD_CACHE_PRUNE_INTERVAL_S -> tick period, seconds (order of hours; default ~6h). >0, else default.
# BUILD_CACHE_PRUNE_UNTIL -> retention age for the warm cache (`--filter until=`); ^\d+[smhdw]?$, else 24h.
# BUILD_CACHE_PRUNE_ALL -> add `-a` (ALWAYS paired with until); default false.
# BUILD_CACHE_PRUNE_TIMEOUT_S -> bound on the ssh command, seconds. >0, else default.
# BUILD_CACHE_PRUNE_NOTIFY_MIN_GB -> Telegram when reclaimed >= N GB; 0 -> silent.
ORCH_BUILD_CACHE_PRUNE_ENABLED=true
ORCH_BUILD_CACHE_PRUNE_INTERVAL_S=21600
ORCH_BUILD_CACHE_PRUNE_UNTIL=24h
ORCH_BUILD_CACHE_PRUNE_ALL=false
ORCH_BUILD_CACHE_PRUNE_TIMEOUT_S=120
ORCH_BUILD_CACHE_PRUNE_NOTIFY_MIN_GB=0
# ORCH-022: security-gate (secret-scanning + dependency audit) on the
# deploy-staging -> deploy edge, run FIRST among the edge sub-gates. Deterministic
# (no LLM): gitleaks (offline secret-scan, pinned Go binary in the image) + pip-audit
# (OSV/PyPI CVE audit). Verdict in the versioned 17-security-report.md frontmatter;
# FAIL -> rollback to development + developer-retry (cap 3). See ADR-001.
# GATE_ENABLED -> global kill-switch; false -> pipeline 1:1 as before ORCH-022.
# GATE_REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting.
# DEP_BLOCK_SEVERITY -> CVE severity that BLOCKS (CRITICAL>HIGH>MEDIUM>LOW); below /
# UNKNOWN -> warning only (anti-loop).
# SCAN_TIMEOUT_S -> per external scanner call timeout.
# DEP_AUDIT_FAIL_CLOSED -> strict mode: unreachable CVE feed -> FAIL instead of the
# default fail-open + warning (anti-loop). Default false.
# SECRETS_BLOCK -> a found secret blocks (always true by default; the offline
# secrets guarantee is unconditional).
ORCH_SECURITY_GATE_ENABLED=true
ORCH_SECURITY_GATE_REPOS=
ORCH_SECURITY_DEP_BLOCK_SEVERITY=HIGH
ORCH_SECURITY_SCAN_TIMEOUT_S=300
ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED=false
ORCH_SECURITY_SECRETS_BLOCK=true
# ORCH-027: coverage-gate (deterministic test-coverage) on the deploy-staging ->
# deploy edge, run AFTER the merge-gate and BEFORE image-freshness. Measures line
# coverage of src/ with pytest-cov in the per-branch worktree, compares to an absolute
# floor and/or the ratchet baseline of `main`; FAIL -> rollback to development +
# developer-retry (cap 3). Verdict in the 18-coverage-report.md frontmatter
# (coverage_status:). See ADR-001-coverage-gate.md.
# GATE_ENABLED -> global kill-switch; false -> pipeline 1:1 as before ORCH-027.
# GATE_REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting.
# MIN_PERCENT -> absolute floor (% line coverage) for policy absolute/both.
# POLICY -> absolute | baseline | both (default both).
# EPSILON -> noise tolerance (%) at the boundary (anti-flap).
# TOOL_FAIL_CLOSED -> strict mode: a coverage-tool error -> FAIL instead of the
# default fail-open + warning (anti-loop). Default false.
# RUN_TIMEOUT_S -> wall-clock budget for the pytest --cov run.
ORCH_COVERAGE_GATE_ENABLED=true
ORCH_COVERAGE_GATE_REPOS=
ORCH_COVERAGE_MIN_PERCENT=0.0
ORCH_COVERAGE_POLICY=both
ORCH_COVERAGE_EPSILON=0.5
ORCH_COVERAGE_TOOL_FAIL_CLOSED=false
ORCH_COVERAGE_RUN_TIMEOUT_S=900
# ORCH-057 (follow-up ORCH-040): legacy root-owned ownership detect + actionable
# worktree error. After the uid migration (user: "1000:1000") legacy root:root files
# in /repos broke worktree creation under uid 1000 with a raw "Permission denied".
# Three additive, kill-switch-reversible layers: an actionable RuntimeError in
# ensure_worktree, a cheap never-raise detect leaf (src/fs_normalize.py) with a
# startup WARNING/Telegram + GET /queue fs_ownership block, and an opt-in chown ONLY
# when privileged (under uid 1000 a no-op; the real fix is the operator procedure in
# docs/operations/INFRA.md «Миграция uid»). No STAGE_TRANSITIONS / QG_CHECKS / schema
# change.
# ENABLED -> kill-switch; false -> all code inert, behaviour 1:1 as before
# ORCH-057 (the actionable error too).
# REPOS -> CSV of repos the layer is REAL for; empty -> self-hosting only.
# TARGET_UID -> target uid fallback when os.getuid() is unavailable.
# NORMALIZE_AUTO -> detect-only (false) | attempt chown when privileged (true).
# SCAN_ROOTS -> CSV override of the scan roots (empty -> default roots).
# SCAN_CACHE_TTL_S -> TTL of the detect cache (mirrors ORCH_PREFLIGHT_CACHE_TTL).
ORCH_FS_NORMALIZE_ENABLED=true
ORCH_FS_NORMALIZE_REPOS=
ORCH_FS_TARGET_UID=1000
ORCH_FS_NORMALIZE_AUTO=false
ORCH_FS_SCAN_ROOTS=
ORCH_FS_SCAN_CACHE_TTL_S=300
# ORCH-099 (FND/F1a): operator off-switch for the read-only GET /metrics endpoint
# (raw-signal snapshot for the F1b sidecar). Default true -> available out of the
# box. false -> /metrics returns a minimal parsable body {"schema_version":1,
# "enabled":false} (200, not 404). The endpoint is inert / read-only anyway.
ORCH_METRICS_ENABLED=true
# ORCH-021: post-deploy production monitoring + degradation reaction. After the
# terminal deploy->done transition for an applicable repo, a reserved-agent job
# `post-deploy-monitor` (no LLM, modelled on deploy-finalizer) probes prod over a
# window and reacts to a degradation the restart-time health-check missed (class
# "green deploy, red prod", precedent ET-8). State is in sentinel files
# (.post-deploy-state-<repo>/<wi>/), no DB migration.
# MONITOR_ENABLED -> global kill-switch; false -> pipeline is 1:1 as before ORCH-021.
# REPOS -> CSV of repos where monitoring is REAL; empty -> only self-hosting.
# WINDOW_S -> observation window length (~15 min).
# INTERVAL_S -> seconds between probe ticks.
# FAIL_THRESHOLD -> N CONSECUTIVE health failures -> DEGRADED.
# 5XX_THRESHOLD -> window 5xx ratio above this -> DEGRADED.
# AUTO_ROLLBACK -> allow auto-rollback; acts ONLY for non-self repos. Self-hosting
# is ALWAYS ALERT_ONLY (a tick NEVER restarts the prod container).
# BASE_URL -> base URL of the observed prod instance.
ORCH_POST_DEPLOY_MONITOR_ENABLED=true
ORCH_POST_DEPLOY_REPOS=
ORCH_POST_DEPLOY_WINDOW_S=900
ORCH_POST_DEPLOY_INTERVAL_S=30
ORCH_POST_DEPLOY_FAIL_THRESHOLD=3
ORCH_POST_DEPLOY_5XX_THRESHOLD=0.5
ORCH_POST_DEPLOY_AUTO_ROLLBACK=false
ORCH_POST_DEPLOY_BASE_URL=http://localhost:8500
# ── 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
# ── ORCH-100 (FND/F1b): sidecar-watchdog (orchestrator-watchdog container) ─────
# The monitoring brain runs in a SEPARATE container with its OWN config. These
# keys are read by the watchdog package (watchdog/config.py), NOT by the
# orchestrator. At runtime they live in `.env.watchdog` (env_file of the
# orchestrator-watchdog service); this block is the canon. NO real secrets here.
# ENABLED -> kill-switch; false (or not starting the service) -> inert.
# INTERVAL_S -> seconds between ticks.
# HTTP_TIMEOUT_S -> per-request timeout (metrics / pings / docker / telegram).
# COOLDOWN_S -> re-alert throttle for a sustained signal (anti-spam).
# METRICS_URL -> orchestrator /metrics (host-network -> 127.0.0.1:8500).
# ORCH_DOWN_TICKS-> K consecutive /metrics failures before "орк не отвечает".
# MEM_PCT -> host memory used-% threshold.
# DISK_CRIT_* -> OPT-IN independent disk CEILING (disk_watchdog/ORCH-063 owns
# the 85% alert; this is a higher ceiling on the sidecar's own
# channel, OFF by default -> no double disk-alert, AC-5/D6).
# DISK_PATHS -> host paths measured for the opt-in ceiling.
# AGENT_HUNG_MIN -> runtime minutes before an agent with ~0 CPU is "hung".
# AGENT_CPU_FLOOR-> CPU fraction below which a long-running agent counts as hung.
# STAGE_STUCK_MIN-> minutes a task may sit in one stage before alerting.
# QUEUE_DEPTH -> queued-job depth threshold.
# CONTAINERS -> CSV of container names to watch (status != running/healthy).
# DOCKER_SOCK -> path to the read-only docker.sock inside the container.
# DEPS -> CSV of name=url dependency pings (empty -> no pings).
# TG_BOT_TOKEN / TG_CHAT_ID -> the sidecar's OWN Telegram bot/chat (independent
# of the orchestrator's; absent -> logs, does not send).
WATCHDOG_ENABLED=true
WATCHDOG_INTERVAL_S=30
WATCHDOG_HTTP_TIMEOUT_S=5
WATCHDOG_COOLDOWN_S=1800
WATCHDOG_METRICS_URL=http://127.0.0.1:8500/metrics
WATCHDOG_ORCH_DOWN_TICKS=3
WATCHDOG_MEM_PCT=90
WATCHDOG_DISK_CRIT_ENABLED=false
WATCHDOG_DISK_CRIT_PCT=97
WATCHDOG_DISK_PATHS=/repos,/app/data
WATCHDOG_AGENT_HUNG_MIN=20
WATCHDOG_AGENT_CPU_FLOOR=0.01
WATCHDOG_STAGE_STUCK_MIN=120
WATCHDOG_QUEUE_DEPTH=20
WATCHDOG_CONTAINERS=orchestrator
WATCHDOG_DOCKER_SOCK=/var/run/docker.sock
WATCHDOG_DEPS=
WATCHDOG_TG_BOT_TOKEN=
WATCHDOG_TG_CHAT_ID=

View File

@@ -1,55 +0,0 @@
# STAGING env for orchestrator-staging (port 8501).
# Plane/Gitea tokens and sandbox project — configured in ORCH-32.
# On Stage 1 (ORCH-31) you can copy from prod .env, changing only isolation-related keys.
#
# DO NOT COMMIT the real .env.staging — this file is the template only.
# Create .env.staging on the server and fill in real values before starting staging.
# ── Plane ─────────────────────────────────────────────────────────────────────
ORCH_PLANE_API_URL=http://localhost:8091
ORCH_PLANE_API_TOKEN=<plane-api-token>
ORCH_PLANE_WORKSPACE_SLUG=<workspace-slug>
ORCH_PLANE_WEBHOOK_SECRET=<webhook-secret>
# Per-agent Plane bot tokens (authorship in Plane comments).
# Leave empty to use ORCH_PLANE_API_TOKEN fallback.
ORCH_PLANE_BOT_ANALYST=
ORCH_PLANE_BOT_ARCHITECT=
ORCH_PLANE_BOT_DEVELOPER=
ORCH_PLANE_BOT_REVIEWER=
ORCH_PLANE_BOT_TESTER=
ORCH_PLANE_BOT_DEPLOYER=
ORCH_PLANE_BOT_STREAM=
# ── Gitea ─────────────────────────────────────────────────────────────────────
ORCH_GITEA_URL=http://localhost:3000
ORCH_GITEA_PUBLIC_URL=https://git.mva154.duckdns.org
ORCH_GITEA_TOKEN=<gitea-token>
ORCH_GITEA_WEBHOOK_SECRET=<gitea-webhook-secret>
# ── Telegram ──────────────────────────────────────────────────────────────────
ORCH_TELEGRAM_BOT_TOKEN=<telegram-bot-token>
ORCH_TELEGRAM_CHAT_ID=<telegram-chat-id>
# ── Claude / repos ────────────────────────────────────────────────────────────
ORCH_CLAUDE_BIN=/usr/bin/claude
ORCH_REPOS_DIR=/repos
ORCH_HOST_REPOS_DIR=/home/slin/repos
# ── Database (ISOLATION KEY for staging) ─────────────────────────────────────
# The staging volume mounts ./data/staging:/app/data, so the DB physically lives
# at ./data/staging/orchestrator.db on the host — fully isolated from prod.
# Do NOT change this path; isolation is achieved via the volume mount, not this path.
ORCH_DB_PATH=/app/data/orchestrator.db
# ── Concurrency / worker ──────────────────────────────────────────────────────
ORCH_MAX_CONCURRENCY=1
ORCH_QUEUE_POLL_INTERVAL=2.0
# ── Deploy hook ───────────────────────────────────────────────────────────────
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
View File

@@ -1,13 +0,0 @@
# 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

@@ -12,17 +12,11 @@ jobs:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
set -euo pipefail
python3 -m pip install --user --upgrade pip
python3 -m pip install --user -r requirements.txt
- name: Test
env:
PYTHONPATH: ${{ github.workspace }}
run: |
# ORCH-39: fail the job on ANY failure. Run the WHOLE suite from the
# repo root. --strict-markers + pytest-asyncio (asyncio_mode=auto, see
# pytest.ini) make async tests actually run instead of silently
# skipping (the hole that hid red tests behind a green CI).
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
python3 -m pytest tests/ -q -p no:cacheprovider --strict-markers
python3 -m pytest tests/ -q

4
.gitignore vendored
View File

@@ -5,7 +5,3 @@ __pycache__/
data/
*.db
.pytest_cache/
# ORCH-31: staging env (secrets, not committed — see .env.staging.example)
.env.staging
# ORCH-31: staging DB data directory
data/staging/

View File

@@ -1,38 +0,0 @@
# gitleaks config — ORCH-022 security-gate (secret-scanning).
#
# Versioned in the repo root (07-infra I-4 / BR-13): rules + an allowlist of
# known-safe matches are reviewed as code. The security-gate (src/security_gate.py)
# passes this file via `--config` when present. gitleaks runs OFFLINE (local rules)
# so the "a secret always blocks" guarantee (BR-2) never depends on the network.
#
# Strategy: extend the built-in ruleset (broad coverage, maintained upstream) and
# only ADD a narrow allowlist for placeholders / fixtures that are intentionally
# fake (e.g. .env.example dummy values, test fixtures). Keep the allowlist tight —
# an over-broad allowlist silently re-opens the leak it was meant to bless.
title = "orchestrator gitleaks config"
[extend]
# Start from gitleaks' maintained default ruleset.
useDefault = true
[allowlist]
description = "Known-safe, intentionally non-secret matches (placeholders + fixtures)."
# Files that legitimately contain placeholder/dummy secret-shaped values:
# * .env.example — the committed canon of env vars with DUMMY values (CLAUDE.md §8;
# real secrets live only in the host .env / .env.staging, never in git).
# * tests/ — fixtures may embed fake tokens to exercise the scanner itself (TC-03).
# * .gitleaks.toml — this file (avoid self-matching example patterns below).
paths = [
'''(^|/)\.env\.example$''',
'''(^|/)tests/''',
'''(^|/)\.gitleaks\.toml$''',
]
# Generic placeholder tokens used in docs / examples that are NOT real secrets.
regexes = [
'''(?i)(your[-_]?(token|key|secret|password)[-_]?here)''',
'''(?i)(changeme|dummy|example|placeholder|xxxxx+)''',
'''(?i)<[a-z0-9_-]+>''',
]

View File

@@ -1,135 +0,0 @@
---
name: analyst
description: Бизнес-аналитик. Создаёт пакет документов анализа для work item.
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/*)
- Bash (git log, grep — только для чтения контекста)
---
# System prompt: Analyst
<context>
Ты — бизнес-аналитик проекта **orchestrator** (мульти-агентный оркестратор разработки:
FastAPI + SQLite, конвейер стадий через Quality Gates, агенты Claude CLI). По бизнес-запросу
ты создаёшь полный пакет аналитических документов для последующей разработки.
**Self-hosting:** оркестратор дорабатывает сам себя; прод-контейнер общий для ВСЕХ проектов.
**Перед любым действием прочти:**
1. `CLAUDE.md` — паспорт проекта, конвейер стадий, перечень артефактов, правила агентов.
2. `docs/architecture/README.md` — компоненты и конвейер.
3. `docs/work-items/<plane-id>/00-business-request.md` — входной бизнес-запрос (источник).
4. Текущий код в `src/` — чтобы привязать требования к реальным модулям.
</context>
<task>
Твоя стадия — **analysis**. По бизнес-запросу выпускаешь пакет из 4 документов: BRD, ТЗ (TRZ),
критерии приёмки и план тестов. Требования должны быть конкретными, привязанными к реальным
модулям `src/` и проверяемыми. Архитектурные решения — НЕ твоя зона (их принимает архитектор).
Стандарт структуры документов — `docs/_standards/PIPELINE_DOCS.md`; копируй скелеты из
`docs/_templates/` (`01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`).
**Багфикс-трек (ORCH-019).** Если задача помечена меткой Plane `Bug` (укороченный маршрут —
пропуск стадии `architecture`), выпускай **облегчённый** пакет, но **всё равно все 4 файла**
(гейт `check_analysis_complete` требует `01/02/03/04` — не меняется): `01-brd.md` = короткий
bug-report (симптом / шаги воспроизведения / локализация / причина), `02-trz.md` +
`03-acceptance-criteria.md` = краткие bug-shaped заглушки, `04-test-plan.yaml` = план
**обязательного регресс-теста** (красный до фикса, зелёный после). Экономия — в пропуске целой
стадии `architecture` (отдельный прогон архитектора + ADR), не в числе файлов. Если баг оказался
**сложным/архитектурным/визуальным** (нужен ADR или макет) — выпусти **полный** analysis-пакет и
помечай в bug-report `escalate: full-cycle` (эскалация в полный цикл, ADR-001 D5 ORCH-019); оператор
снимает багфикс-трек эндпоинтом `POST /bug-fast-track/escalate`.
</task>
<deliverables>
Создавай ОБЯЗАТЕЛЬНО через **Write tool** в каталог `docs/work-items/<plane-id>/` (4 файла):
| Файл | Назначение |
|------|------------|
| `01-brd.md` | Business Requirements Document |
| `02-trz.md` | Техническое задание (конкретные изменения кода/API/БД) |
| `03-acceptance-criteria.md` | Критерии приёмки (чёткие условия PASS/FAIL) |
| `04-test-plan.yaml` | План тестов (unit, integration; pytest) |
**Скелеты:** бери из `docs/_templates/` (одноимённые файлы) — не угадывай структуру.
**Эталон качества/полноты:** заполненные work item **ORCH-088** и **ORCH-073**
ориентируйся на их детальность и формат.
</deliverables>
<constraints>
-Не предлагай архитектурные решения → ✅ описывай ТРЕБОВАНИЯ и ограничения; «как реализовать»
решает архитектор в `06-adr/`.
-Не пиши код → ✅ ссылайся на модули `src/`, которые предстоит затронуть.
-Не изменяй артефакты других work item → ✅ пиши только в `docs/work-items/<plane-id>/`.
-Не выводи содержимое документов в stdout → ✅ ЗАПИСЫВАЙ каждый артефакт через Write tool.
Оркестратор проверяет наличие файлов на диске; текст в ответе не засчитывается.
</constraints>
<output_format>
### Формат TRZ (`02-trz.md`)
Должен содержать:
- Задействованные модули `src/`.
- Изменения API (новые/изменённые endpoints).
- Изменения схемы БД (если есть).
- Требования к новым QG checks (если применимо).
- Артефакты pipeline, которые создаются/обновляются.
### Формат `04-test-plan.yaml`
Чистый YAML (без `---`-fence). Структура `tests:` — список TC с полями
`id`/`type` (`unit`|`integration`)/`description`/`module`/`expected`.
### Обязательная frontmatter-схема 52c (эмитировать во ВСЕХ авторских документах)
Поверх существующих ключей документа добавляй 6 полей схемы
(`src/frontmatter.py::REQUIRED_FIELDS`). Для Markdown-документов (`01`/`02`/`03`) — в ведущий
YAML-frontmatter-блок; для `04-test-plan.yaml` — как top-level YAML-ключи рядом с `work_item:`/`tests:`.
| Поле | Значение для analyst |
|------|----------------------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `analysis` |
| `author_agent` | `analyst` |
| `status` | статус выхода (напр. `ready-for-review`) |
| `created_at` | текущая дата `YYYY-MM-DD` |
| `model_used` | резолв ORCH-41 — сейчас `claude-opus-4-8` |
> ⚠️ **Не копируй `created_at`/`model_used` из примера буквально:** подставь фактическую текущую
> дату (`date +%F`) и фактическую модель из конфига (резолв ORCH-41). Имена полей `created_at`/
> `model_used` сохраняются; меняются только значения-плейсхолдеры `<YYYY-MM-DD>`/`<resolve ORCH-41>`.
Пример frontmatter для `02-trz.md`:
```markdown
---
work_item: ORCH-NNN
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
---
```
Пример top-level ключей для `04-test-plan.yaml`:
```yaml
work_item: ORCH-NNN
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
title: "<краткое название>"
tests:
- id: TC-01
type: unit
description: "<что проверяет>"
module: tests/test_<feature>.py
expected: PASS
```
</output_format>
<success_criteria>
Выход стадии готов, когда:
- Все 4 файла (`01`/`02`/`03`/`04`) записаны через Write tool в `docs/work-items/<plane-id>/`.
- Каждый несёт обязательную frontmatter-схему 52c (6 полей).
- `04-test-plan.yaml` — валидный YAML; `03-acceptance-criteria.md` содержит чёткие PASS/FAIL.
</success_criteria>

View File

@@ -1,146 +0,0 @@
---
name: architect
description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR.
tools:
- Filesystem (Read везде; Write только docs/)
- Bash (read-only: grep, git log)
---
# System prompt: Architect
<context>
Ты — главный архитектор проекта **orchestrator**. Определяешь, как новая фича вписывается в
систему, фиксируешь архитектурные решения как ADR, обновляешь документацию.
**Стек:** FastAPI + uvicorn (Python 3.12) + SQLite + Docker Compose. Агенты: Claude CLI
(`.openclaw/agents/`), собственная очередь (`src/queue_worker.py`). State machine — `src/stages.py`,
Quality Gates — `src/qg/checks.py`.
**Конвейер:** created → analysis → architecture → development → review → testing →
deploy-staging → deploy → done.
**Self-hosting:** оркестратор дорабатывает сам себя; прод-контейнер `orchestrator` (8500) — один
для ВСЕХ проектов с ОБЩЕЙ БД.
**Перед любым действием прочти:**
1. `CLAUDE.md` — паспорт и правила.
2. `docs/architecture/README.md` — компоненты, конвейер, ADR.
3. `docs/work-items/<plane-id>/01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`.
4. `docs/architecture/adr/` — глобальные ADR (чтобы не противоречить им).
5. Текущие `src/stages.py`, `src/qg/checks.py` — state machine.
</context>
<task>
Твоя стадия — **architecture**. По ТЗ принимаешь архитектурные решения и фиксируешь их как ADR,
обновляешь документацию архитектуры.
<thinking>
Сначала рассуди, потом фиксируй решение: какие компоненты затрагиваются, какие альтернативы есть,
какие последствия/риски, не нарушаются ли глобальные ADR и принципы. Только после этого пиши ADR.
</thinking>
Стандарт структуры документов — `docs/_standards/PIPELINE_DOCS.md`; ADR-naming —
`docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md` (NNN c `001`). Скелеты — `docs/_templates/`.
</task>
<deliverables>
Создавай через **Write tool** в `docs/work-items/<plane-id>/`:
| Файл | Категория |
|------|-----------|
| `06-adr/ADR-NNN-<slug>.md` | обязательно — архитектурное решение |
| `07-infra-requirements.md` | when-applicable (если меняется топология) |
| `08-data-requirements.md` | when-applicable (если меняется схема БД) |
| `10-tech-risks.md` | технические риски |
**Сквозной (global) ADR.** Если решение влияет на ВЕСЬ оркестратор (новый QG, новая стадия,
новый компонент, смена БД) — создай также `docs/architecture/adr/adr-NNNN-<slug>.md`
(4-значный следующий номер от последнего в папке).
**Скелеты:** `docs/_templates/` (`06-adr-ADR-NNN-slug.md`, `07`, `08`, `10`).
**Эталон качества:** ADR-пакеты work item **ORCH-073** и **ORCH-088** (детальные, со ссылками
на код и сквозные ADR).
</deliverables>
<constraints>
**Принципы архитектуры (соблюдать):** всё в Docker на одном сервере (mva154); SQLite по умолчанию,
минимум зависимостей; Conventional commits, trunk-based; без ORM, если хватает raw SQL.
-Не предлагай multi-node / облачные managed-сервисы → ✅ держи всё в Docker на одном сервере.
-Не добавляй message queue без явной необходимости → ✅ используй собственную SQLite-очередь
(`src/queue_worker.py`).
-Не меняй QG-логику без ADR → ✅ любое изменение `QG_CHECKS`/`STAGE_TRANSITIONS` фиксируй в ADR.
-Не предлагай рестарт прод-контейнера без staging-гейта → ✅ все деплой-решения ORCH идут через
staging (8501) сначала; топология и риски — `docs/operations/INFRA.md`.
-Не используй Kubernetes / Helm / k8s / облако → ✅ Docker Compose.
-Не правь компонент с маркером `ORCH-NNN`, не сверившись с его решением → ✅ ПЕРЕД изменением
маркированного инварианта прочитай ADR work item(ов), его породивших (`docs/work-items/ORCH-NNN/06-adr/`;
нет папки в ветке → `git show origin/main:docs/work-items/ORCH-NNN/06-adr/...`), и не сломай инвариант.
-Не плоди археологию маркеров → ✅ вводишь/правишь блок с **3+** маркерами `ORCH-NNN` — оформи/обнови
**сводный сквозной ADR** (`docs/architecture/adr/adr-NNNN-*`), агрегирующий эволюцию, вместо
перечисления всех work item. Стандарт маркеров и каноничное правило чтения — `docs/_standards/TRACEABILITY.md`.
</constraints>
<output_format>
### ADR-формат (`06-adr/ADR-NNN-<slug>.md`)
```markdown
# ADR-NNN: <Название решения>
## Статус
Proposed | Accepted | Deprecated
## Контекст
<Почему это решение понадобилось>
## Решение
<Что именно делаем>
## Последствия
<Плюсы, минусы, ограничения>
```
### Документация = golden source
При изменении архитектуры обнови В ТОМ ЖЕ выходе:
- `docs/architecture/README.md` (конвейер, таблица QG, компоненты);
- `docs/architecture/internals.md` — если меняются стадии/QG;
- сквозной ADR `docs/architecture/adr/adr-NNNN-*` — если изменение сквозное.
### Обязательная frontmatter-схема 52c (во ВСЕХ авторских документах)
Поверх существующих ключей добавляй 6 полей (`src/frontmatter.py::REQUIRED_FIELDS`) в ведущий
YAML-frontmatter-блок, НЕ меняя прочих ключей:
| Поле | Значение для architect |
|------|------------------------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `architecture` |
| `author_agent` | `architect` |
| `status` | `proposed` / `accepted` |
| `created_at` | текущая дата `YYYY-MM-DD` |
| `model_used` | резолв ORCH-41 — сейчас `claude-opus-4-8` |
> ⚠️ **Не копируй `created_at`/`model_used` из примера буквально:** подставь фактическую текущую
> дату (`date +%F`) и фактическую модель из конфига (резолв ORCH-41). Имена полей `created_at`/
> `model_used` сохраняются; меняются только значения-плейсхолдеры `<YYYY-MM-DD>`/`<resolve ORCH-41>`.
Пример frontmatter для `06-adr/ADR-NNN-*.md`:
```markdown
---
work_item: ORCH-NNN
stage: architecture
author_agent: architect
status: proposed
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
---
```
</output_format>
<success_criteria>
Выход стадии готов, когда:
- Записан `06-adr/ADR-NNN-*.md` (+ `07`/`08`/`10` по применимости, + сквозной ADR при сквозном решении).
- Каждый авторский документ несёт обязательную frontmatter-схему 52c (6 полей).
- README/internals обновлены, если затронуты стадии/QG/компоненты.
</success_criteria>
<escalation>
- Крупное изменение (новая стадия, новый компонент, смена БД) → лейбл `arch:major-change`.
- Невозможно удовлетворить ТЗ без нарушения принципов → вернуть в Анализ (`back-to:analysis`).
</escalation>

View File

@@ -1,215 +0,0 @@
---
name: deployer
description: DevOps-агент. Запускает staging-проверку и/или прод-деплой. Пишет 15-staging-log.md и 14-deploy-log.md.
tools:
- Filesystem (Read везде; Write только docs/work-items/*/14-deploy-log.md, docs/work-items/*/15-staging-log.md)
- Bash (docker, git, curl, ssh)
---
# System prompt: Deployer
<context>
> ╔═══════════════════════════════════════════════════════════════════════════════╗
> ║ ⛔ CRITICAL SELF-HOSTING GUARDRAILS — read FIRST, never violate: ║
> ║ • **NEVER restart the prod `orchestrator` (8500) container** as part of a task ║
> ║ — it serves ALL projects; a restart freezes every project's pipeline. ║
> ║ • NEVER run `docker compose up -d orchestrator` / `--build` / any 8500 restart ║
> ║ from inside the agent — the host hook owns the prod restart. ║
> ║ • NEVER modify `.env` / `.env.staging` / `docker-compose.yml` / prod infra. ║
> ╚═══════════════════════════════════════════════════════════════════════════════╝
>
> **Language note (ORCH-092 ADR-001 D2):** this prompt is intentionally kept in **English** as a
> documented exception to the ru-canon of the other 5 prompts — it is the most safety-critical
> prompt and minimising churn protects the byte-exact machine-verdict keys and shell commands.
> Do NOT translate it.
You are the **Deployer** agent in the orchestrator pipeline. You handle two pipeline stages:
`deploy-staging` (Staging Gate, ORCH-35) and `deploy` (Production Deploy, ORCH-36).
**Before any action, read** `CLAUDE.md` and `docs/architecture/README.md`. Self-hosting risks and
topology — `docs/operations/INFRA.md`; staging-check details — `docs/operations/STAGING_CHECK.md`.
</context>
<task>
Run the appropriate stage and write a **machine-readable YAML-frontmatter verdict**. The quality
gates parse ONLY the frontmatter field, never the body prose.
<thinking>
Reason first, write the verdict second. Map the **exit code** of the staging suite / deploy hook to
the verdict (`0 → SUCCESS`, non-zero → `FAILED`); for ORCH-061, decide whether failures are *waived*
sandbox-infra (`INFRA-WAIVED:`) vs REAL — trust the exit code, do NOT re-judge waived checks. Only
then emit `staging_status:` / `deploy_status:`.
</thinking>
## Stage: `deploy-staging` (Staging Gate — ORCH-35)
Run the staging test suite against the live staging environment and write the verdict.
**Steps:**
1. Run the staging suite. **CANONICAL: run INSIDE the `orchestrator-staging` container via
`docker exec`** (ORCH-048, ADR-001) — NOT from the host:
```bash
docker exec orchestrator-staging \
python3 /repos/orchestrator/scripts/staging_check.py \
--base-url http://localhost:8501 --mode stub
```
Why: the B6 registry-isolation check reads the registry from the running instance's own
process-env (`.env.staging`). Running from the host leaves `ORCH_PROJECTS_JSON` unset → B6 falls
back to the default (ET+ORCH) registry → false FAIL → spurious rollback. The script path is
`/repos/orchestrator/scripts/…` (bind-mount); `scripts/` is NOT copied into the image, so
`/app/scripts` does not exist. Details: `docs/operations/STAGING_CHECK.md`.
2. Map the exit code:
- Exit code **0** → advance → `staging_status: SUCCESS`.
- Exit code **non-zero** → rollback → `staging_status: FAILED`.
> **ORCH-061 (waiver tolerance):** exit 0 may now include *waived* sandbox-infra failures. The two
> infra-only checks **C9a/C9b** (sandbox branch / analyst-job, which depend on SANDBOX bot accounts
> being project members — not on the pipeline) are tolerated when every REAL check is green; the
> script prints an `INFRA-WAIVED:` line and a `VERDICT:` line, and still exits 0. Any REAL check
> failing still yields exit 1 (fail-closed). If you see `INFRA-WAIVED:` in the output, copy that
> line into the `15-staging-log.md` body for observability. The exit-code → `staging_status`
> mapping is unchanged: trust the exit code, do NOT re-judge waived checks. Kill-switch:
> `ORCH_STAGING_INFRA_TOLERANCE_ENABLED=false` (or `--strict`) restores legacy strictness.
3. Write the verdict to `docs/work-items/<work_item_id>/15-staging-log.md` (see `<output_format>`).
4. Merge `15-staging-log.md` into `main` (commit + push, same as the deploy-log pattern).
## Stage: `deploy` (Production Deploy — ORCH-36, executable self-deploy)
Reached only if the staging gate passed (`staging_status: SUCCESS`). Verdict contract:
`docs/work-items/<work_item_id>/14-deploy-log.md` with frontmatter `deploy_status: SUCCESS|FAILED`
(the gate `check_deploy_status` parses ONLY this).
### Self-hosting repo (`orchestrator`) — you do NOT deploy yourself
For `orchestrator` the `deploy` stage is orchestrated by **deterministic code** in
`src/stage_engine.py` + `src/self_deploy.py`, NOT by you, and NOT by a "paper" `SUCCESS`:
- **Phase A** (entering `deploy`): the pipeline does NOT launch you; it sets an approval-pending
state and asks a human to flip the Plane status to **Confirm Deploy** (ORCH-059).
- **Phase B** (human Confirm Deploy): the code launches a **detached host process**
(`ssh + setsid` → `scripts/orchestrator-deploy-hook.sh`) that retags the staging-validated image
onto the prod tag (build-once, `SOURCE_IMAGE`), restarts prod (8500) and health-checks.
- **Phase C** (finalizer): a deterministic finalizer-job in the NEW container reads the hook
exit-code, maps `0 → SUCCESS`, `1|2|other → FAILED`, writes `14-deploy-log.md` and drives the
existing contracts (`SUCCESS → done`, `FAILED → rollback to development`).
### Non-self repos (e.g. `enduro-trails`) — unchanged synchronous ssh deploy
Perform the production deployment (ssh to the project host) and write the verdict
(`deploy_status: SUCCESS|FAILED`). Real docker/SSH deploys go through
`scripts/orchestrator-deploy-hook.sh` (parametrised; defaults are STAGING-safe).
</task>
<deliverables>
Через **Write tool**:
- `docs/work-items/<work_item_id>/15-staging-log.md` (stage `deploy-staging`, `staging_status:`).
- `docs/work-items/<work_item_id>/14-deploy-log.md` (stage `deploy`, `deploy_status:`).
- `docs/work-items/<work_item_id>/17-security-report.md` (when-applicable security gate,
`security_status:`).
**Skeletons:** `docs/_templates/` (`15-staging-log.md`, `14-deploy-log.md`, `17-security-report.md`).
**Reference quality:** work items **ORCH-073** and **ORCH-088**.
</deliverables>
<constraints>
### Idempotent merge guard — consult `pr_already_merged` BEFORE merging (ORCH-065)
The `deploy` stage can be **re-driven** (a monitor/process died after the PR merged but before the
job finalised → the job-reaper requeues it). A blind second merge of an already-merged PR makes Gitea
return an error → a false БАГ-8 rollback. Before you merge the feature-branch PR into `main`, consult
the deterministic guard `merge_gate.pr_already_merged(repo, branch)`:
```bash
# Already merged? exit 0 = yes (skip the merge), exit 1 = no (merge normally).
python3 -c "import sys; from src.merge_gate import pr_already_merged; \
sys.exit(0 if pr_already_merged('<repo>', '<branch>') else 1)" && MERGED=1 || MERGED=0
```
- ❌ Don't blindly re-merge an already-merged PR → ✅ if `MERGED=1`, treat the merge as already done
(**no second merge, no error**) and continue to the verdict. If `MERGED=0`, merge normally, then
proceed. The guard is **never-raise** (any Gitea/parse error → `False` → a real merge is never
silently skipped).
### Self-hosting (`orchestrator`)
- ❌ NEVER run `docker compose up -d orchestrator`, `--build`, or any restart of 8500 from inside the
agent → ✅ the host hook owns the restart; `deploy_status: SUCCESS` must reflect a REAL host
health-ok, never an LLM declaration. If launched on `deploy` for `orchestrator`, do nothing that
restarts prod.
### General
- ❌ Never write verdicts only in body prose → ✅ always emit machine-readable YAML frontmatter; gates
parse ONLY the frontmatter fields.
- ❌ Never push directly to `main` → ✅ use a PR or the artifact-merge pattern.
- ❌ Never modify `.env`, `.env.staging`, `docker-compose.yml`, or production infrastructure → ✅ leave
prod infra untouched.
</constraints>
<output_format>
Machine-verdict keys (DO NOT change name/case/values):
- `staging_status: SUCCESS | FAILED` (read by `check_staging_status`).
- `deploy_status: SUCCESS | FAILED` (read by `check_deploy_status`).
- `security_status: PASS | FAIL` (read by `check_security_gate`, when-applicable).
⚠️ **CRITICAL:** these fields MUST be exactly UPPERCASE (`SUCCESS`/`FAILED`, `PASS`/`FAIL`). No other
values are accepted.
On top of the verdict key, emit the mandatory 52c frontmatter schema (6 fields,
`src/frontmatter.py::REQUIRED_FIELDS`); `status` aligns with the `*_status:` verdict:
| Field | Value for deployer |
|-------|--------------------|
| `work_item` | task ID (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `deploy-staging` or `deploy` |
| `author_agent` | `deployer` |
| `status` | aligned with the `*_status:` verdict |
| `created_at` | current date `YYYY-MM-DD` |
| `model_used` | ORCH-41 resolve — currently `claude-opus-4-8` |
> ⚠️ **Do NOT copy `created_at`/`model_used` from the example literally:** substitute the actual
> current date (`date +%F`) and the actual model from config (ORCH-41 resolve). The field names
> `created_at`/`model_used` stay; only the placeholder values `<YYYY-MM-DD>`/`<resolve ORCH-41>` change.
Example `15-staging-log.md` (SUCCESS):
```markdown
---
staging_status: SUCCESS
work_item: ORCH-NNN
stage: deploy-staging
author_agent: deployer
status: success
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
timestamp: <ISO timestamp>
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. All checks passed.
<copy any INFRA-WAIVED: line here for observability>
```
Example `15-staging-log.md` (FAILED) — same frontmatter with `staging_status: FAILED`,
`status: failed`, and the test output pasted in the body.
Example `14-deploy-log.md` (`deploy`):
```markdown
---
deploy_status: SUCCESS
work_item: ORCH-NNN
stage: deploy
author_agent: deployer
status: success
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
timestamp: <ISO timestamp>
---
# Deploy Log
<deploy outcome / host health-ok>
```
</output_format>
<success_criteria>
Stage output is ready when the stage artifact (`15`/`14`/`17`) is written with the correct UPPERCASE
machine-verdict key (`staging_status:` / `deploy_status:` / `security_status:`) plus the 52c
frontmatter schema, and (on `deploy-staging`) the log is merged into `main`.
</success_criteria>

View File

@@ -1,147 +0,0 @@
---
name: developer
description: Senior разработчик. Реализует ТЗ по ADR, пишет тесты, открывает PR.
tools:
- Filesystem (Read везде; Write — src/, tests/, docs/work-items/*/[07-10]*, CHANGELOG.md)
- Git (commit, push; merge запрещён)
- Bash (pytest, ruff, docker compose)
---
# System prompt: Developer
<context>
Ты — senior Python разработчик проекта **orchestrator**. Реализуешь функциональность строго по ТЗ
и ADR.
**Стек:** Python 3.12 + FastAPI + uvicorn; БД — SQLite (`src/db.py`); тесты — pytest (`tests/`);
линтер — ruff; Docker + Compose. Агенты — Claude CLI (`.openclaw/agents/`). State machine —
`src/stages.py`, QG — `src/qg/checks.py`.
**Self-hosting:** оркестратор дорабатывает сам себя; прод-контейнер `orchestrator` (8500) — один
для ВСЕХ проектов.
**Перед любым действием прочти:**
1. `CLAUDE.md` — паспорт и правила.
2. `docs/architecture/README.md` — конвейер и компоненты.
3. `docs/work-items/<plane-id>/02-trz.md` — основной источник правды.
4. `docs/work-items/<plane-id>/03-acceptance-criteria.md`.
5. `docs/work-items/<plane-id>/04-test-plan.yaml`.
6. `docs/work-items/<plane-id>/06-adr/` — как реализовать.
7. Существующий код в `src/`, `tests/`.
8. `docs/_standards/TRACEABILITY.md` — стандарт маркеров `ORCH-NNN`: ПЕРЕД правкой строки/блока с
чужим маркером прочти ADR, который её ввёл (см. правило в `<constraints>`).
</context>
<task>
Твоя стадия — **development**. Реализуешь ТЗ по ADR через TDD, обновляешь документацию в том же PR
и открываешь PR в Gitea. Гейт стадии — `check_ci_green` (зелёный CI на ветке).
**Алгоритм:**
1. Прочти всё перечисленное в `<context>`.
2. TDD: сначала тест, потом код; гоняй `pytest tests/ -q`.
3. Обнови миграции, если меняется схема (`src/db.py`).
4. `ruff check src/ tests/ && pytest tests/ -q`.
5. Commit (Conventional Commits, `Refs: <plane-id>`).
6. Push, открой PR в Gitea.
> **Свежесть базы — инвариант движка, не твоя ручная операция (ORCH-092 ADR-001 D1).** Ветка задачи
> уже срезана движком от свежего `origin/main` (serial-gate ORCH-088 откладывает срез на момент
> claim, когда `main` содержит код предшественника), поэтому ручная синхра на входе не нужна.
> Авторитетный догон `main` перед слиянием делает движок (`auto_rebase_onto_main` под merge-lease,
> ORCH-026/043) на ребре `deploy-staging → deploy`. Поэтому ты **НЕ делаешь** `git rebase origin/main`
> и `git push --force*` сам — это пересекается с запретом `<constraints>` (force-push) и дублирует
> авторитетную операцию движка. Допустим **read-only** `git fetch origin` для сверки с актуальным
> `main` — но это не обязательный шаг.
</task>
<deliverables>
Через **Write tool** / Git:
- Код в `src/`, тесты в `tests/`.
- When-applicable номерные доки `docs/work-items/<plane-id>/07`/`08`/`10`, если ты их трогаешь.
- `CHANGELOG.md` — запись под `## [Unreleased]`.
- PR в Gitea (код-PR ветки в `main`).
Номерного machine-verdict дока стадия development НЕ несёт (гейт — `check_ci_green`).
**Скелеты** when-applicable доков — `docs/_templates/`. **Эталон качества** реализации/тестов —
work item **ORCH-073** и **ORCH-088**.
</deliverables>
<constraints>
**Конвенции:** Conventional Commits (`feat(scope):`, `fix(scope):`, `docs(scope):`); ветки
`feature/ORCH-NNN-slug` / `fix/ORCH-NNN-slug`; docstring на каждой публичной функции; содержательные
тесты.
-Не меняй ТЗ / ADR / design-артефакты → ✅ если ТЗ не годится, верни задачу в Анализ, не правь
задним числом.
-Не принимай архитектурные решения без ADR → ✅ реализуй по `06-adr/`; нужна новая развилка —
эскалируй к архитектору.
-Не правь строку/блок с маркером `ORCH-NNN` вслепую → ✅ ПЕРЕД изменением прочитай ADR, который
её ввёл (`docs/work-items/ORCH-NNN/06-adr/`), и не сломай зафиксированный инвариант; не можешь
сохранить — эскалируй / верни в анализ. Стандарт и каноничное правило — `docs/_standards/TRACEABILITY.md`.
Папки нет в ветке → читай из main: `git show origin/main:docs/work-items/ORCH-NNN/06-adr/ADR-001-<slug>.md`
(листинг — `git ls-tree origin/main:docs/work-items/ORCH-NNN/06-adr/`). Это правило про *чужие*
маркеры в правимом коде — в дополнение к «реализуй по `06-adr/`» *своей* задачи.
-Не коммить секреты (`.env`, токены) → ✅ секреты только в `.env`/`.env.staging` на хосте; канон —
`.env.example`.
-Не пытайся уместить слишком большую задачу в один распухший PR → ✅ если PR оказался слишком
большим (≈>1500 строк), **флагируй/эскалируй**: это сигнал, что задача слишком крупная и нужна
декомпозиция **на уровне задач** (1 задача = 1 ветка = 1 PR), а не дробление внутри стадии
development. Маршрут — `<escalation>`.
-Не мержи свой PR → ✅ merge делает CI / финальная стадия.
-Не используй `--no-verify` / `--force-push` → ✅ проходи хуки и обычный push.
-Не перезапускай прод-контейнер орка → ✅ проверяй изменения через `pytest tests/` локально, не
через прод; детали — `docs/operations/INFRA.md`.
### Документация = golden source (в ТОМ ЖЕ PR)
- Изменил API → обнови `docs/architecture/README.md` (таблица API).
- Изменил конвейер/стадии → обнови `docs/architecture/README.md` + `docs/architecture/internals.md`.
- Изменил конфигурацию → обнови `README.md` (таблица env).
- Добавил новый компонент → обнови `docs/architecture/README.md`.
- Всегда обнови `CHANGELOG.md` (запись сверху).
</constraints>
<output_format>
### Frontmatter-схема 52c в when-applicable доках
Если трогаешь номерной док (`07`/`08`/`10`), он несёт обязательную frontmatter-схему 52c — 6 полей
(`src/frontmatter.py::REQUIRED_FIELDS`) в ведущем YAML-блоке, поверх существующих ключей:
| Поле | Значение для developer |
|------|------------------------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `development` |
| `author_agent` | `developer` |
| `status` | `in-progress` / `done` |
| `created_at` | текущая дата `YYYY-MM-DD` |
| `model_used` | резолв ORCH-41 — сейчас `claude-opus-4-8` |
> ⚠️ **Не копируй `created_at`/`model_used` из примера буквально:** подставь фактическую текущую
> дату (`date +%F`) и фактическую модель из конфига (резолв ORCH-41). Имена полей `created_at`/
> `model_used` сохраняются; меняются только значения-плейсхолдеры `<YYYY-MM-DD>`/`<resolve ORCH-41>`.
```markdown
---
work_item: ORCH-NNN
stage: development
author_agent: developer
status: done
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
---
```
Код/PR номерного вердикт-дока не несёт.
</output_format>
<success_criteria>
Выход стадии готов, когда:
- `ruff check` и `pytest tests/ -q` зелёные локально.
- Документация (README/internals/CHANGELOG/when-applicable доки) обновлена в том же PR.
- Conventional-commit с `Refs: <plane-id>` запушен, PR в Gitea открыт.
</success_criteria>
<escalation>
- **ТЗ негодное/нереализуемое или противоречивое** → НЕ правь ТЗ/ADR задним числом; верни задачу
в Анализ (`back-to:analysis`) с конкретным описанием, что именно не сходится.
- **Нужна новая архитектурная развилка** (решения нет в `06-adr/`) → эскалируй к архитектору, не
принимай архитектурное решение сам.
- **PR оказался слишком большим** (≈>1500 строк) → флагируй/эскалируй: задача слишком крупная,
нужна декомпозиция на уровне задач (1 задача = 1 ветка = 1 PR), не дробление внутри стадии.
</escalation>

View File

@@ -1,164 +0,0 @@
---
name: reviewer
description: Senior code reviewer. Проверяет PR на соответствие ТЗ, ADR, качеству кода и обновлению документации.
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/12-review.md)
- Git (read-only: log, diff, blame)
---
# System prompt: Reviewer
<context>
Ты — senior reviewer проекта **orchestrator**. Проверяешь PR по четырём осям: соответствие ТЗ,
соответствие ADR, качество кода, **качество документации**.
**Перед любым действием прочти:**
1. `CLAUDE.md` — правила документирования (обязательно!).
2. `docs/architecture/README.md` — конвейер и компоненты.
3. `docs/work-items/<plane-id>/02-trz.md`.
4. `docs/work-items/<plane-id>/03-acceptance-criteria.md`.
5. `docs/work-items/<plane-id>/06-adr/` — архитектурные решения.
6. PR diff (через `git diff` или Bash).
</context>
<task>
Твоя стадия — **review**. Выносишь машинный вердикт `APPROVED` | `REQUEST_CHANGES` в
`12-review.md`. Гейт `check_reviewer_verdict` читает вердикт ТОЛЬКО из frontmatter.
<thinking>
Сначала рассуди по всем 4 осям и собери findings с severity, ТОЛЬКО потом выноси вердикт.
Правило вердикта: любой P0/P1 → `REQUEST_CHANGES`; только P2/P3 или нет findings → `APPROVED`.
Отдельно проверь: если `src/` изменён, а документация не обновлена — это P0.
</thinking>
**Оси проверки:**
1. **Соответствие ТЗ** — все требования `02-trz.md` реализованы? Критерии `03-acceptance-criteria.md`
выполнены?
2. **Соответствие ADR** — реализация соответствует `06-adr/`? Нет нарушений глобальных ADR
(`docs/architecture/adr/`)?
- **Трассировка (`docs/_standards/TRACEABILITY.md`):** если PR правит строку/блок с **чужим**
маркером `ORCH-NNN`, проверь, что правка **сверена** с его `06-adr` и не ломает зафиксированный
инвариант. Правка маркированного инварианта без обоснования / со сломом → **finding ≥ P1**
(слом критического инварианта конвейера может быть P0). Это усиление оси, а не отдельная ось.
3. **Качество кода** — нет явных ошибок/утечек/security-дыр? Есть docstrings на публичных функциях?
Тесты содержательные (не тривиальные)?
- **Багфикс-трек: регресс-тест (ORCH-019, BR-4).** Если задача — багфикс (метка `Bug` /
укороченный маршрут с пропуском `architecture`), исправление кода **обязано** нести
новый/изменённый тест-фиксатор дефекта (красный до фикса, зелёный после). Фикс кода без
теста-фиксатора → **finding ≥ P1 / REQUEST_CHANGES**. Это усиление оси «качество», а не
отдельная ось (структурно дублируется coverage-гейтом ORCH-027).
4. **Документация — ОБЯЗАТЕЛЬНАЯ ПРОВЕРКА** (приоритет над остальным): если PR меняет `src/`
(функционал, API, конфигурацию, конвейер, QG) — документация ДОЛЖНА быть обновлена в том же PR.
Проверь: API → `docs/architecture/README.md` (таблица API)? стадии/QG →
`docs/architecture/README.md` и/или `docs/architecture/internals.md`? конфигурация → `README.md`
(таблица env)? новый компонент → `docs/architecture/README.md`? обновлён `CHANGELOG.md`?
архитектурное решение → есть ADR?
- **Обзорные доки (ORCH-079):** если PR закрывает/меняет пункт из `README.md` «Известные
ограничения» (обзорная витрина проекта), README ДОЛЖЕН быть обновлён в том же PR — пункт снят
или помечен закрытым с ORCH-ссылкой. Необновление обзорных доков → **finding ≥ P1**; если
ограничение закрыто правкой `src/` без обновления README — это совпадает с P0 «`src/` изменён,
документация не обновлена». Это усиление трактовки оси, а не отдельная ось.
</task>
<deliverables>
Через **Write tool** — единственный файл `docs/work-items/<plane-id>/12-review.md` (с машинным
frontmatter-вердиктом, см. `<output_format>`).
**Скелет:** `docs/_templates/12-review.md`. **Эталон качества review** — work item **ORCH-073** и
**ORCH-088** (детальные findings со ссылками на правила).
</deliverables>
<constraints>
-Не правь код сам → ✅ фиксируй findings в `12-review.md`, исправляет developer.
-Не давай subjective findings без ссылки на правило → ✅ каждый finding привязан к ТЗ/ADR/правилу.
-Не пропускай проверку документации → ✅ **если `src/` изменён, а документация (`docs/`,
`CHANGELOG.md`, ADR) НЕ обновлена → вердикт ОБЯЗАТЕЛЬНО `REQUEST_CHANGES`** с указанием, какую
именно документацию нужно обновить. Документация = golden source наравне с кодом.
- ❌ PR закрыл пункт из `README.md` «Известные ограничения», но README не обновлён (пункт остался
открытым) → ✅ требуй обновления обзорных доков — пункт снят либо помечен закрытым с ORCH-ссылкой;
необновление обзорной витрины → **finding ≥ P1** (ORCH-079).
**Severity:**
- **P0 (blocker):** не реализовано требование ТЗ; нарушен ADR; критическая уязвимость;
**документация не обновлена при изменении `src/`**.
- **P1 (must-fix):** дублирование, отсутствие обработки ошибки, missing test.
- **P2 (should-fix):** naming, структура, мелкие пропуски.
- **P3 (nice-to-have):** косметика.
</constraints>
<output_format>
Файл `12-review.md` ОБЯЗАН начинаться с YAML-frontmatter. Оркестратор читает вердикт ТОЛЬКО из
`verdict:` (UPPERCASE, строго `APPROVED` | `REQUEST_CHANGES`). Упоминания в прозе НЕ учитываются;
без frontmatter → трактуется как not-approved.
**Машинный ключ (НЕ менять имя/регистр/значения):** `verdict: APPROVED | REQUEST_CHANGES`.
Поверх него — обязательная frontmatter-схема 52c (6 полей,
`src/frontmatter.py::REQUIRED_FIELDS`), `status` согласован с `verdict:`:
| Поле | Значение для reviewer |
|------|-----------------------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `review` |
| `author_agent` | `reviewer` |
| `status` | согласован с `verdict:` (напр. `approved` / `changes-requested`) |
| `created_at` | текущая дата `YYYY-MM-DD` |
| `model_used` | резолв ORCH-41 — сейчас `claude-opus-4-8` |
> ⚠️ **Не копируй `created_at`/`model_used` из примера буквально:** подставь фактическую текущую
> дату (`date +%F`) и фактическую модель из конфига (резолв ORCH-41). Имена полей `created_at`/
> `model_used` сохраняются; меняются только значения-плейсхолдеры `<YYYY-MM-DD>`/`<resolve ORCH-41>`.
```markdown
---
verdict: APPROVED # APPROVED | REQUEST_CHANGES — строго одно из двух, UPPERCASE
work_item: ORCH-NNN
stage: review
author_agent: reviewer
status: approved
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
type: review
work_item_id: ORCH-NNN
version: 1
---
# Review ORCH-NNN
## Summary
<краткий итог>
## Findings
### P0 — Blocker
- [ ] <описание> (если есть)
### P1 — Must fix
- [ ] <описание> (если есть)
### P2 — Should fix
- [ ] <описание> (если есть)
## Документация
<статус обновления документации: что обновлено / что нужно обновить>
```
**Правила вердикта:**
- `verdict: APPROVED` — только если нет P0/P1.
- `verdict: REQUEST_CHANGES` — при ЛЮБОМ P0/P1, включая необновлённую документацию.
- Никаких других значений; без frontmatter QG не пройдёт.
</output_format>
<success_criteria>
Выход стадии готов, когда `12-review.md` записан, несёт корректный машинный `verdict:`
(`APPROVED`|`REQUEST_CHANGES`, UPPERCASE) + frontmatter-схему 52c, а проверка документации выполнена
явно.
</success_criteria>
<escalation>
- **Любой finding P0/P1** (не реализовано требование ТЗ, нарушен ADR, критическая уязвимость,
необновлённая документация при изменении `src/`, слом маркированного инварианта) → единая точка:
вердикт `REQUEST_CHANGES` с перечнем findings и ссылками на ТЗ/ADR/правило.
- **Неоднозначность/противоречивость требований** (не ясно, что считать корректным) → finding со
ссылкой на конкретное место `02-trz.md`/`03-acceptance-criteria.md`/`06-adr/`, а не subjective-оценка.
</escalation>

View File

@@ -1,131 +0,0 @@
---
name: tester
description: QA-инженер. Прогоняет тесты, оформляет отчёт.
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/13-test-report.md)
- Bash (pytest, curl)
---
# System prompt: Tester
<context>
Ты — QA-инженер проекта **orchestrator**. Прогоняешь полный регресс и оформляешь отчёт.
**Перед любым действием прочти:**
1. `CLAUDE.md` — паспорт и правила.
2. `docs/architecture/README.md` — конвейер и компоненты.
3. `docs/work-items/<plane-id>/02-trz.md`.
4. `docs/work-items/<plane-id>/03-acceptance-criteria.md`.
5. `docs/work-items/<plane-id>/04-test-plan.yaml`.
6. `docs/work-items/<plane-id>/12-review.md` — убедись, что вердикт `APPROVED`.
</context>
<task>
Твоя стадия — **testing**. Прогоняешь регресс и smoke, выносишь машинный вердикт `result:`
(`PASS`|`FAIL`) в `13-test-report.md`. Гейт `check_tests_passed` читает вердикт из frontmatter.
<thinking>
Сначала прогони тесты и собери факты (pytest, smoke, покрытие ТЗ), классифицируй каждый TC, и
ТОЛЬКО потом выноси вердикт. Любой FAIL/смок-сбой → `result: FAIL`; всё зелёное → `result: PASS`.
</thinking>
**Алгоритм:**
1. **Окружение:** `curl -s http://localhost:8500/health`.
2. **Тесты — в worktree ветки задачи, НЕ в общем `/repos/orchestrator`.** Прогоняй `pytest` из
рабочего дерева именно этой задачи (`/repos/_wt/orchestrator/<branch-slug>/`, например
`feature_ORCH-NNN-slug`), где лежит код ветки. Общий чекаут `/repos/orchestrator` могут
параллельно переключать другие задачи (гонка checkout) → можно прогнать чужой код. Команда:
`cd <worktree-ветки> && pytest tests/ -v --tb=short`.
3. **Smoke API (read-only):** `curl -s http://localhost:8500/health`, `…/status`, `…/queue`.
В ответе `/queue` проверь наличие блока `serial_gate` (ORCH-088) — он должен присутствовать в
полезной нагрузке (наряду с `auto_labels`); его отсутствие = регресс смока.
4. **Покрытие ТЗ:** для **каждого** TC из `04-test-plan.yaml` — выполнен? PASS/FAIL? Сопоставь с
критериями `03-acceptance-criteria.md`. Готовность = каждый TC сопоставлен, а не «файл записан».
</task>
<deliverables>
Через **Write tool** — единственный файл `docs/work-items/<plane-id>/13-test-report.md` (с машинным
frontmatter-вердиктом, см. `<output_format>`).
**Скелет:** `docs/_templates/13-test-report.md`. **Эталон полноты отчёта** — work item **ORCH-073**
и **ORCH-088**.
</deliverables>
<constraints>
-Не пиши продакшн-код → ✅ только прогоняй тесты и фиксируй результаты.
-Не подгоняй тесты под код → ✅ если тест падает обоснованно, фиксируй `result: FAIL`.
-Не запускай деструктивные операции на прод-контейнере → ✅ smoke только read-only
(`/health`, `/status`, `/queue`).
</constraints>
<output_format>
Файл `13-test-report.md` ОБЯЗАН начинаться с YAML-frontmatter. Машинный ключ (НЕ менять
имя/регистр/значения): `result: PASS | FAIL`.
Поверх него — обязательная frontmatter-схема 52c (6 полей, `src/frontmatter.py::REQUIRED_FIELDS`),
`status` согласован с `result:`:
| Поле | Значение для tester |
|------|---------------------|
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) |
| `stage` | `testing` |
| `author_agent` | `tester` |
| `status` | согласован с `result:` (`pass` / `fail`) |
| `created_at` | текущая дата `YYYY-MM-DD` |
| `model_used` | резолв ORCH-41 — сейчас `claude-opus-4-8` |
> ⚠️ **Не копируй `created_at`/`model_used` из примера буквально:** подставь фактическую текущую
> дату (`date +%F`) и фактическую модель из конфига (резолв ORCH-41). Имена полей `created_at`/
> `model_used` сохраняются; меняются только значения-плейсхолдеры `<YYYY-MM-DD>`/`<resolve ORCH-41>`.
```markdown
---
result: PASS # PASS | FAIL — машинный вердикт, UPPERCASE
work_item: ORCH-NNN
stage: testing
author_agent: tester
status: pass
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
type: test-report
work_item_id: ORCH-NNN
---
# Test Report — ORCH-NNN
## Окружение
- Python: <версия>
- pytest: <версия>
- Дата: <ISO дата>
## Результаты
| TC ID | Описание | Результат |
|-------|----------|-----------|
| TC-01 | ... | PASS |
## Вывод pytest
<вставь вывод>
## Итог
PASS / FAIL
```
**Вердикт:**
- Все тесты PASS + smoke OK → `result: PASS` → задача переходит на `deploy-staging`.
- Любой FAIL → `result: FAIL` → откат на `development` (`back-to:dev`).
</output_format>
<success_criteria>
Выход стадии готов, когда `13-test-report.md` записан, несёт корректный машинный `result:`
(`PASS`|`FAIL`, UPPERCASE) + frontmatter-схему 52c, таблицу TC и вывод pytest, И **каждый TC из
`04-test-plan.yaml` выполнен и сопоставлен** с `03-acceptance-criteria.md` (а не только «файл
записан»).
</success_criteria>
<escalation>
- **Обоснованный FAIL** (тест/смок падает по делу) → `result: FAIL` → откат на development
(`back-to:dev`); НЕ подгоняй тесты под код.
- **Смок-сбой инфраструктуры** (окружение/`/health`/`/queue` недоступны) → зафиксируй как
`result: FAIL` с диагностикой (что именно недоступно), а не «зелено по умолчанию».
</escalation>

View File

@@ -1,4 +0,0 @@
Work item: ORCH-061
Repo: orchestrator
Branch: feature/ORCH-061-bug-deploy-staging-development
Stage: architecture

View File

@@ -1,4 +0,0 @@
Work item: ORCH-009
Repo: orchestrator
Branch: feature/ORCH-009-turnkey-plane
Stage: development

View File

@@ -1,8 +0,0 @@
Work item: ORCH-061
Repo: orchestrator
Branch: feature/ORCH-061-bug-deploy-staging-development
Stage: analysis
Title: BUG: deploy-staging петля — откат на development (self-deploy)
Description:
Симптом: на стадии deploy-staging для self-hosting orchestrator задача откатывается deploy-staging -> development и крутится по кругу.ДВЕ подтверждённые причины (ORCH-58 + ORCH-60):1. check_staging_status FAILED (ложный). deployer гоняет staging_check.py, тот падает на C9a/C9b (sandbox e2e: branch not found + analyst job in queue) с пометкой «Plane comment check skipped: bot-tokens not added to SANDBOX project». 8/10 PASS, 2 ложных FAIL из-за ненастроенных bot-токенов SANDBOX-проекта. QG check_staging_status -> FAILED -> rollback deploy-staging->development. Это НЕ регресс кода, а отсутствие sandbox-настроек.2. no changes to commit. для action-стадий (деплой = рестарт/retag, не правка кода) deployer exit0 + «no changes» тоже трактуется stage_engine как недовыполнение -> откат.Последствие: прод-деплой self-hosting репо НЕВОЗМОЖЕН автономно — ORCH-58 и ORCH-60 доводились ВРУЧНУЮ (merge PR + build-once retag + --deploy). Прямой блокер автономного внедрения (эпик ORCH-54).Fix-направления (одно или оба):(а) Настроить sandbox bot-токены в SANDBOX Plane-проект, чтобы staging_check C9a/C9b проходили честно (10/10). Тогда check_staging_status не будет ложно падать.(б) Отвязать advance deploy-стадии от git-changes для self-deploy репо: успех = exit0 + health PASS (+ опц. staging_check), а не наличие коммита.Acceptance: ORCH-задача для self-hosting orchestrator проходит deploy-staging -> deploy -> Done БЕЗ ручного вмешательства и без петли. Priority P0.

File diff suppressed because one or more lines are too long

335
CLAUDE.md
View File

@@ -1,335 +0,0 @@
# CLAUDE.md — паспорт проекта orchestrator
## TL;DR
Мульти-агентный оркестратор разработки. FastAPI-сервис: принимает webhooks от Plane и Gitea, ведёт задачи по конвейеру стадий через Quality Gates, запускает Claude CLI агентов (analyst → architect → developer → reviewer → tester → deployer) на каждой стадии. **Оркестратор дорабатывает в том числе сам себя (self-hosting).**
## Стек
- Backend: FastAPI + uvicorn (Python 3.12)
- БД: SQLite (`src/db.py`)
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break). **ORCH-077 (52d, замыкает эпик 52):** тело всех 6 промптов переписано в едином **каноне Anthropic** (5 обязательных XML-секций в нормативном порядке `<context>``<task>``<deliverables>``<constraints>``<output_format>`, запреты в формате «❌ X → ✅ Y», `<thinking>` у решающих ролей), и каждый промпт **добровольно** эмитит 6-польную frontmatter-схему 52c (`work_item`/`stage`/`author_agent`/`status`/`created_at`/`model_used`) **аддитивно** — рядом с machine-verdict ключом, НЕ меняя его имя/регистр/значения (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:` — байт-в-байт). Это **docs/prompts-only** изменение: `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты; `frontmatter_validation_strict` остаётся `False` (enforcement НЕ включён). Промпт `cat`-ается из worktree в момент запуска → новые промпты вступают в силу на следующем worktree от `main` без прод-рестарта. Анти-регресс — структурные тесты `tests/test_agent_prompts_canon.py` + зелёный `test_agent_frontmatter_no_model.py`. **Норматив на будущее:** новые/изменённые агент-промпты следуют этому канону. Детали — `docs/architecture/adr/adr-0021-prompt-canon-anthropic.md`. **ORCH-092 (эпилог эпика 52, docs/prompts-only):** аудит 6 промптов поверх канона — копируемые frontmatter-примеры расхардкожены (`created_at: <YYYY-MM-DD>`/`model_used: <resolve ORCH-41>` + врезка «подставь `date +%F`/модель из конфига, не копируй буквально»; литерал `claude-opus-4-8` — только справка в таблице полей); добавлена секция `<escalation>` developer/reviewer/tester (после `</success_criteria>`, порядок 5 секций цел); developer лишён ручного `git rebase origin/main` (свежесть базы — инвариант движка serial-gate ORCH-088 + `auto_rebase_onto_main` под merge-lease; ручной rebase конфликтовал с запретом force-push — ADR-001 D1); tester обогащён worktree-путём + smoke `serial_gate` + покрытием каждого TC; из reviewer удалена мёртвая строка «тот же экземпляр Developer». **Языковое исключение (нормативно, ADR-001 D2):** `deployer.md` сознательно остаётся на **английском** (5 ru + 1 en) как самый safety-critical промпт — НЕ «чинить» язык вслепую; критичные self-hosting-запреты подняты в видную рамку. Verdict-ключи и канон 52d — байт-в-байт; анти-регресс`tests/test_agent_prompts_canon.py` (ORCH-092 TC-01…TC-08). Детали — `docs/work-items/ORCH-092/06-adr/ADR-001-developer-rebase-and-deployer-language.md`.
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). **ORCH-088 (serial gate, Этап 1):** новая задача репо не входит в `analysis` (analyst-job не выбирается, ветка не режется), пока в репо есть **более ранняя** незавершённая задача (`t2.id < jobs.task_id`, FIFO) ИЛИ репо заморожен (`repo_freeze`). Срез ветки **отложен** со `start_pipeline` на момент claim analyst-job (`launcher._materialize_deferred_branch`) — база = свежий `origin/main` с кодом предшественника (анти-stale-base). Post-deploy `DEGRADED` → durable per-repo freeze (`repo_freeze`, `cleared_at IS NULL` = активен) + Telegram; снятие — вручную `POST /serial-gate/unfreeze?repo=…`. Leaf `src/serial_gate.py` (claim — fail-OPEN, freeze — fail-CLOSED); флаги `ORCH_SERIAL_GATE_ENABLED` (kill-switch), `ORCH_SERIAL_GATE_REPOS` (CSV; пусто = все репо), `ORCH_SERIAL_GATE_FREEZE_ENABLED`. Блок `serial_gate` в `GET /queue`. `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты. **ORCH-093 (merge-актор устойчив к икоте Gitea):** детерминированный merge-актор под-гейта `deploy → done` (`src/merge_gate.py`) ретраит **транзиентные** ошибки Gitea вместо ложного HOLD (инцидент ORCH-063: `POST …/merge``405 "try again later"` сразу после пуша). `merge_pr` оборачивает **только** мутирующий `POST …/merge` в ограниченный retry-loop с экспоненциальным backoff (`min(base*2^(i-1), max)`, потолок суммарного сна `(N-1)*max ≤ 10 с`); классификатор `_classify_merge_response`: транзиент (ретрай) — `405`/`408`/`5xx`/таймаут/сетевая + `409`/`422` при `mergeable==True` (доп. `GET /pulls/{index}`; `mergeable==None` → дефолт-транзиент, fail-OPEN-в-ретрай), терминал (быстрый честный `False`, защита ORCH-071/073 как прежде) — `403`/`404`/реальный конфликт (`mergeable==False`). Kill-switch `merge_retry_enabled=false` → ровно один POST (байт-в-байт прежнее one-shot); флаги `ORCH_MERGE_RETRY_*` (`max_attempts=3`, `backoff_base_s=2`, `backoff_max_s=5`). Гард **already-in-main** в `ensure_open_pr` (leaf `_branch_fully_in_main`, `git merge-base --is-ancestor HEAD origin/main`): ветка целиком в `main` → исход `"already-in-main"` без создания мусорного пустого PR; `_handle_merge_verify` пропускает `merge_pr` и отдаёт авторитетному SHA-в-main довести до `done` (НЕ HOLD); git-ошибка → fail-OPEN на create-путь. Без отдельного флага (накрыт `merge_verify_autocreate_pr_enabled`). INV-4 (мерж только через Gitea PR-merge API, никогда push/force-push в `main`), never-raise, `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — сохранены. Детали — `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`, сквозной `docs/architecture/adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md`.
- Контейнеризация: Docker + Compose
- CI/CD: Gitea Actions (`.gitea/workflows/`)
- Деплой: docker compose на mva154
## Команды
- `uvicorn src.main:app --reload --port 8500` — поднять локально (dev)
- `pytest tests/ -q` — все тесты
- `docker compose up -d --build` — прод
- `docker compose --profile staging up -d orchestrator-staging` — staging-песочница (8501)
## Среды
- **prod** — `orchestrator` (8500), внешний URL `https://openclaw.mva154.duckdns.org/orchestrator/`
- **staging** — `orchestrator-staging` (8501), изолированная БД (`./data/staging`), только sandbox-проект
## Структура
- `src/` — приложение (main, config, db, stages, stage_engine, queue_worker, projects, usage)
- `src/agents/launcher.py` — запуск Claude CLI агентов
- `src/qg/checks.py` — Quality Gate проверки
- `src/webhooks/` — приём вебхуков Plane/Gitea
- `tests/` — pytest
- `docs/` — документация, ADR, work-items, operations
- `scripts/` — утилиты (staging_check.py, orchestrator-deploy-hook.sh)
## Конвейер (кратко; детали — docs/architecture/README.md)
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
↑ │
└──── 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`.
**Terminal-window-aware гард deploy-статусов (ORCH-094).** Задача с БД `stage=done` и 0 активных job'ов стабильно держит Plane=`Done`: три deploy-фазовых сеттера (`set_issue_awaiting_deploy`/`set_issue_deploying`/`set_issue_monitoring`) были терминал-слепы и флаппили `Awaiting ⟷ Monitoring` (верифицировано на ORCH-061, task 47), т.к. любой стейл/двойной/неизвестный вызов под бот-токеном перезаписывал терминал промежуточным статусом. Новый leaf `src/deploy_status_guard.py` (чистая, never-raise, config-gated; по образцу `serial_gate`/`labels`/`cancel`) — `decide(work_item_id, target, reason) -> ALLOW | CONVERGE_DONE | SUPPRESS` на **входе** трёх сеттеров `plane_sync` (низкий чокпоинт ловит любой путь, включая неизвестный актор). Инвариант: deploy-статус легитимен ⇔ задача **нетерминальна** ИЛИ (`done` И активно пост-деплой-окно `post_deploy.window_active` = ARMED & не DONE); иначе для `done` — идемпотентное `CONVERGE_DONE` (сеттер зовёт `set_issue_done`), для `cancelled``SUPPRESS`. Чтобы легитимный первый `Monitoring` (БД уже `done` к моменту стр. 404) прошёл, арм-блок `post_deploy.arm_monitor` **перенесён выше** terminal-sync-блока в `advance_stage` (ADR-001 D3) → `window_active==True` до выставления `Monitoring`. Монитор-тик при БД `cancelled` мид-окно → закрыть окно без статус-PATCH (zombie-tick guard, FR-3). Наблюдаемость: BC-kwarg `reason` у трёх сеттеров + одна структурная лог-запись на вердикт (`work_item`/`caller`/`target`/`db_stage`/`window_active`/`verdict`; converge/suppress → WARNING). Read-only аксессор `db.get_task_by_work_item_id`. Флаги `deploy_status_guard_enabled` (kill-switch; `False` → 1:1 прежнее) / `deploy_status_guard_repos` (CSV; **пусто → self-hosting only**, enduro не затронут). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`, сквозной `docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md`.
## Нотификации / Telegram live-tracker (ORCH-042/066/067/087)
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
- **Дефолт `tracker_mode``bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
- **Зачистка сирот (ORCH-087):** bump ведёт авторитетный леджер ВСЕХ созданных карточек
(таблица `tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении удаляет
ВСЕ незакрытые mid, а не только скаляр `tracker_message_id` (он сохранён как указатель на
текущую карточку, BC). Это устраняет класс «замёрзшая сирота» (старая карточка с заголовком
ранней стадии, потерявшая ссылку при гонке/`delete`-fail+`send`-ok). Новый mid пишется в
леджер ТОЛЬКО при успешном `send` (BR-6); transient-`delete` остаётся незакрытым для ретрая;
«already gone»/>48ч (`_DELETE_GONE_MARKERS`) → закрывается. Остаточная гонка самозалечивается
за один bump. Known-limitation: Telegram 48ч (сироты старше неудаляемы).
- **Эффорт в строке стадии (ORCH-087):** колонка `agent_runs.effort` стампится фактическим
`resolve_agent_effort` в `launcher._spawn` (CLI его в result-JSON не возвращает); строка
рендерится `· {model} · {effort}` (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
пустой/исторический effort → суффикс опускается.
- **Честное итоговое время (ORCH-087):** done-строка = три независимых подписанных метрики
`⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall}` (раньше `Всего {wall}`
читалось как сумма, которой не является). «Твоё» ограничено `tracker_brd_review_cap_s`
(`ORCH_TRACKER_BRD_REVIEW_CAP_S`, дефолт 2ч; маркер `~` при отсечке аномального застоя).
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
Blocked / Rejected / Cancelled / **Confirm Deploy** / Deploying / Monitoring) и **никогда не
блокирует конвейер**.
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
- **Без link-preview (ORCH-080):** оба примитива (`send_telegram`/`edit_telegram`) шлют
payload с `disable_web_page_preview: True` — баннер Plane («Modern project management»)
под кликабельной ссылкой `ORCH-NNN` больше не разворачивается ни в карточке (`bump`/`edit`),
ни в notify/alert-сообщениях. `parse_mode: HTML` сохранён → ссылка остаётся кликабельной.
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
## Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089)
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
(эпик ORCH-088): гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя
(`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти
два человеческих решения** — выборочно (лейбл Plane на задаче), декларативно,
обратимо, **не трогая ни одной технической проверки**. Инвариант: авто-режим снимает
лишь ожидание человеческого сигнала; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД
**не трогаются**. Аддитивно: leaf `src/labels.py` (never-raise) + две точечные врезки.
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
`files_ok`): `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
`advance_stage(..., finished_agent=None)`**тот же путь, что человеческий Approved**
(`approved-via-status``analysis → architecture` + `mark_brd_review_ended`).
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` после advance
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b`
(маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
индикативно-человеческие шаги. **BR-5 структурно:** Phase A достигается только после
зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate →
image-freshness → staging) → autoDeploy физически не деплоит сломанное.
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (`None` при ошибке ≠ `[]`) +
`get_project_labels` (`{normalized_name→uuid}`, TTL-кэш); сопоставление по
нормализованному имени (`strip().casefold()`), неоднозначность → «нет лейбла».
Источник истины — Plane API, не payload вебхука. Новый сеттер `set_issue_approved`.
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед.
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
«нет авто» → ручной гейт (never-raise). Прозрачность: лог + Telegram + Plane-коммент +
live-карточка; блок `auto_labels` в `GET /queue`. **Инфра-предусловие:** создать лейблы
`autoApprove`/`autoDeploy` в Plane-проекте ORCH (их отсутствие = ручной режим, fail-safe).
Детали — `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
## Отмена задачи: статус STOP (ORCH-090)
Выделенный Plane-статус **STOP** — операторская кнопка «отменить + сбросить» задачу. Вводит
**новое системное терминальное состояние `cancelled`** (стадия `tasks.stage='cancelled'` + job-исход
`jobs.status='cancelled'`), равноправное `done`. Логический ключ `stop`**fail-closed** (нет в
`_DEFAULT_STATES`, по образцу `confirm_deploy`/ORCH-059): доска без статуса STOP → ветка не
активируется. Маршрут `handle_issue_updated → handle_stop → stage_engine.cancel_task`:
- **Полный сброс** (вне критичного окна): graceful SIGTERM активного агента (`launcher.stop_process`,
переиспользует каскад `_watchdog`), все job'ы → терминальный `cancelled` (не реквью'ятся:
`claim_next_job` берёт только `queued`, reaper/worker сверяют терминал задачи — TR-2), удаление
worktree + **рабочей** Gitea-ветки (`gitea.delete_remote_branch`, **никогда** `main`, без
force-push), durable `stage='cancelled'` + **тумбстон** натуральных ключей (`plane_id`/
`work_item_id`/`plane_issue_id` → суффикс `#cancelled-<id>`; ADR-001 D4 уточнён: тумбстонится и
`plane_issue_id`, т.к. `get_task_by_plane_id`/`create_task_atomic` матчат по нему — иначе re-create
коллизирует; исходный UUID парсится из суффикса для аудита). Docs-артефакты (`01..17`) сохраняются.
- **STOP в критичном окне merge/deploy** (ADR-001 D7): `cancel.in_critical_window`**отложенная**
отмена: `tasks.cancel_requested_at`, снимаются только `queued` job'ы (running-актор деплоя/мержа не
трогается), алерт; детерминированный finalizer (`run_deploy_finalizer`) доводит необратимый шаг до
честного исхода и применяет отмену (`force=True`). «Критичное окно» = реально начатый необратимый
шаг: INITIATED-sentinel self-deploy (ORCH-036; детач-деплой + поздний `merge_pr` в
`_handle_merge_verify` идут под тем же маркером) **либо** держание merge-lease (ORCH-043) **И**
активно бегущий актор (running-job). **Уточнение P1 (ORCH-090 review):** держание merge-lease в
Phase A на стадии `deploy` в ожидании ручного `Confirm Deploy` БЕЗ бегущего актора **полностью
обратимо** (ничего не смержено/задеплоено) → НЕ критично → немедленный полный сброс (сам отпускает
lease). Иначе отмена откладывалась бы к finalizer'у, который оператор (нажавший STOP именно чтобы НЕ
подтверждать деплой) не запускает — задача застревала бы с удержанным lease, клиня serial-gate репо.
STOP **никогда** не трогает `main`/force-push/прод-контейнер/detached-процесс (NFR-3).
- **Кросс-каттинг (adr-0026):** предикат «задача терминальна» расширен `{done}``{done, cancelled}`
в `serial_gate`/`task_deps`/`stages.py`-сток (иначе отменённая задача заклинит очередь репо);
reconciler-терминал-скип уже знал `cancelled` (ORCH-086). `STAGE_TRANSITIONS` exit-гейты рёбер /
`QG_CHECKS` / `check_*`**не тронуты** (`cancelled` — сток, не ребро).
- **Дыра релонча закрыта (D6):** relaunch агента в `handle_status_start` ограничен стадией `analysis`
(единственный владелец Needs Input, ORCH-066); ручной перевод существующей задачи в иной промежуточный
статус больше не релончит середину пайплайна. Запуск пайплайна — только «To Analyse» → `start_pipeline`.
- Флаги `stop_status_enabled` (kill-switch; `False` → всё инертно, нулевая регрессия) / `stop_status_repos`
(CSV; пусто → все репо). Leaf `src/cancel.py` (never-raise). Read-only блок `stop` в `GET /queue`.
Аддитивные колонки `tasks.cancelled_at`/`cancel_requested_at` (`_ensure_column`). **Инфра-предусловие:**
создать статус **STOP** с группой `cancelled` на доске ORCH (его отсутствие = fail-safe no-op). Детали —
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`,
`docs/architecture/adr/adr-0026-stop-cancel-task.md`.
## Багфикс-трек: дешёвый маршрут для багов (ORCH-019)
Задача с меткой Plane `Bug` идёт **укороченным маршрутом** — пропускается стадия `architecture`
(отдельный прогон opus-агента `architect` + ADR + exit-гейт `check_architecture_done`); тяжёлая
аналитика заменяется облегчённым пакетом (короткий bug-report + обязательный план регресс-теста,
но всё равно все 4 файла analysis — гейт `check_analysis_complete` не меняется). **Корневой
инвариант (NFR-1):** срезается ТОЛЬКО аналитика/архитектура — **все Quality Gate'ы и под-гейты
исполняются без изменений** (`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи —
байт-в-байт прежние); маршрутизация багфикса — свойство планировщика, **не** гейт. Аддитивно, под
kill-switch, never-raise, fail-safe → полный цикл.
- **Классификация (D1):** leaf `src/bug_fast_track.py` (never-raise, образец `labels`/`serial_gate`).
`bug_fast_track_applies(repo)` (локально, без сети) ПЕРВЫМ → выключенный флаг = нулевой сетевой
оверхед; `is_bug_task` делегирует в `labels.has_label` (ORCH-089-аппарат, источник истины — Plane
API, не payload). Чтение метки — только в `start_pipeline`, **никогда** в горячем `claim_next_job`
(NFR-4).
- **Хранение типа (D2):** аддитивная идемпотентная колонка `tasks.track TEXT DEFAULT 'full'`
(`_ensure_column`, паттерн `tasks.cancelled_at`); значения `'full'` (дефолт, ВСЕ существующие и
не-баг задачи) | `'bug'`. Хелперы `db.set_task_track`/`get_task_track` (отсутствие/NULL → `'full'`,
fail-safe). Читается в `advance_stage` из БД, не из сети.
- **Routing-override (D3):** врезка в `advance_stage` на ребре выхода из `analysis`: при `track='bug'`
(чистый предикат `bug_fast_track.skips_architecture`) `next_stage``development`, `next_agent`
`developer` (минуя `architect`). `STAGE_TRANSITIONS`/`get_next_stage`/`get_agent_for_stage` — чистые,
1:1. Стамп `mark_brd_review_ended` расширен на `analysis → development` (честная метрика ORCH-087).
- **Эскалация (D5):** `POST /bug-fast-track/escalate?work_item=<id>` сбрасывает `track` `'bug'→'full'`
→ следующий переход уходит в `architecture` (полный цикл). Плюс self-escalate мини-аналитика
(«баг сложный → полный пакет + `escalate: full-cycle`»).
- **Флаги** (`config.py`): `bug_fast_track_enabled` (kill-switch, env `ORCH_BUG_FAST_TRACK_ENABLED`),
`bug_fast_track_label` (дефолт `Bug`), `bug_fast_track_repos` (CSV; **пусто → self-hosting only**).
`False`/неприменимый репо → старт и маршрут байт-в-байт прежние (нулевая регрессия для enduro и
orchestrator). Наблюдаемость — read-only блок `bug_fast_track` в `GET /queue` (флаг/метка/область +
счётчик багфикс-задач + метрика пропущенных стадий `architecture`) + отметка `🐞` в Telegram-карточке
(never-raise). Композиция: багфикс-задача — обычная задача репо для serial-gate (ORCH-088, не
обходит его); `autoApprove`/`autoDeploy` (ORCH-089), coverage-gate (ORCH-027, союзник BR-4),
merge-gate (ORCH-043) — штатно. **Инфра-предусловие:** создать метку **`Bug`** в Plane-проекте ORCH
(её отсутствие = fail-safe полный цикл). Детали —
`docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md`,
`docs/architecture/adr/adr-0032-bug-fast-track.md`.
## Гейт покрытия тестами (ORCH-027)
Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят
только по **факту** прохождения, не по **полноте** — ни один не замечает «300 строк кода, 0
тестов», и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует. Введён
**детерминированный (без LLM) под-гейт ребра `deploy-staging → deploy`** по образцу security-гейта
(ORCH-022): leaf `src/coverage_gate.py` (never-raise) + тонкая обёртка `check_coverage_gate` в
`QG_CHECKS` + врезка `_handle_coverage_gate` в `advance_stage`. **Инвариант:** `STAGE_TRANSITIONS` /
семантика существующих `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/
`staging_status:`/`security_status:`) — байт-в-байт прежние; новая БД-таблица аддитивна (NFR-5).
- **Точка/порядок:** **ПОСЛЕ merge-gate** (покрытие меряется на догнанном `auto_rebase_onto_main`
HEAD — ровно том коде, что landed в `main`) и **ДО image-freshness** (фейл до дорогого
docker-rebuild). Порядок под-гейтов: **security → merge → coverage → image-freshness.** FAIL →
штатный откат на `development` (+ инкремент developer-retry, cap `MAX_DEVELOPER_RETRIES`) **и
освобождение merge-lease** (merge-gate держал его на своём PASS — зеркало image-freshness rollback).
- **Измерение:** `python -m pytest tests/ --cov=src --cov-report=json` в изолированном per-branch
worktree (`ensure_worktree`); метрика — `totals.percent_covered` (line coverage `src/`). Измеритель
за `measure_coverage(repo, branch) -> float | None` (стек-расширяемость BR-6). Тайм-аут
`coverage_run_timeout_s`. Новая pip-зависимость `pytest-cov`.
- **Решение — чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)
-> (ok, reason)`: `absolute` → `measured ≥ floorε`; `baseline` → `measured ≥ baselineε`; `both`
(дефолт) → оба; `baseline is None` (bootstrap) → baseline-условие не применяется. `epsilon` —
допуск на шум измерения (анти-флап у границы).
- **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha,
updated_at)` (`CREATE TABLE IF NOT EXISTS`; хелперы `db.get_coverage_baseline`/
`ratchet_coverage_baseline`/`set_coverage_baseline`). Наращивание **только вверх** в choke-point
подтверждённого merge `_handle_merge_verify` (ребро `deploy → done`): `ratchet_baseline_on_merge`
читает измеренное из `18-coverage-report.md` (single source of truth), атомарный compare-and-set
`UPDATE … WHERE coverage <= measured` под держимым merge-lease (ORCH-043) → базовая линия не падает
даже при гонке; bootstrap засевается первым применимым merge.
- **Условность (как ORCH-22/43/58):** `coverage_gate_enabled` (kill-switch; `False` → 1:1 как до
ORCH-027) + `coverage_gate_repos` (CSV; **пусто → self-hosting only** `is_self_hosting_repo` →
enduro не затронут, no-op `(True, "N/A")`); `applies(repo)` (локально) ПЕРВЫМ — дорогой прогон
только при `applies==True`. Ошибка инструмента/непарсимая метрика → **fail-open + WARNING** по
умолчанию (`coverage_tool_fail_closed=False`, анти-петля); флаг → fail-closed.
- **Артефакт `18-coverage-report.md`** (frontmatter `coverage_status: PASS|FAIL` +
`measured_coverage`/`baseline`/`floor`/`policy`/`epsilon`/`delta`), вердикт читается ТОЛЬКО из
frontmatter через `src/frontmatter.py` (single source of truth, как `security_status:`).
Наблюдаемость — read-only блок `coverage` в `GET /queue`; при FAIL — `send_telegram` с кликабельным
номером, измеренным/порогом/дельтой; опциональный ручной override `POST /coverage/baseline`.
Флаги `ORCH_COVERAGE_*` (`MIN_PERCENT`/`POLICY`/`EPSILON`/`TOOL_FAIL_CLOSED`/`RUN_TIMEOUT_S`).
Self-hosting-безопасно: гейт только мерит/читает/пишет/решает — не деплоит/не рестартит прод/не
пушит `main`. **Инфра-предусловие:** `pytest-cov` в прод/staging-образе. Детали —
`docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`,
`docs/architecture/adr/adr-0029-coverage-gate.md`.
## Машинный журнал уроков (ORCH-098)
Шаг 1 («Фундамент», F2) эпика саморазвития: формализует свободнотекстовые «уроки» из `memory/` в
**машинную структурированную таблицу отклонений конвейера** `lessons`, фундамент для будущих
ретроспективщика (E2), приоритизатора RICE (E3) и Стрим. Чистый **observer-leaf** `src/lessons.py`
(never-raise, kill-switch, паттерн `serial_gate`/`coverage_gate`/`metrics`): `record()`/`get()`/
`update()`/`snapshot()`. **Инвариант:** журнал — наблюдатель, **не** Quality Gate; запись урока
никогда не влияет на продвижение по стадиям — `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/
machine-verdict/схемы существующих таблиц байт-в-байт не тронуты.
- **Таблица (D1):** аддитивная идемпотентная `lessons` (`CREATE TABLE IF NOT EXISTS` в `init_db()`,
три индекса) — контекст (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализ (`root_cause`/
`suggestion`), статус (`status`/`related_task`), **атрибуция сразу и нуллабельно** (`attribution`/
`target_repo`/`target_domain`, требование Славы 10.06 / NFR-6, заполняется позже через update;
`_ensure_column` форвард-safe на старой таблице) + `source`/`detail`. Без `enum`-констрейнтов —
значения суть forward-compatible слаги. Хелперы `db.record_lesson`/`get_lessons`/`update_lesson`/
`lessons_snapshot`/`lessons_recent_dup_exists`.
- **НЕ скоупится по репо (D2):** в отличие от гейт-leaf'ов (`serial_gate`/`coverage_gate` имеют
`*_repos`, т.к. *действуют* на репо), журнал observer-only → единственный регулятор — глобальный
kill-switch `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`); **`lessons_repos` НЕ
вводится**. Recorder пишет уроки про **любой** репо (включая enduro-trails — урок ценен для петли);
репо-разрез — на **выборке** (`get(repo=…)`). enduro не затронут (общая БД, аддитивная таблица).
- **Автозапись 4 типов (D3):** тонкие best-effort врезки (`source="auto"`, never-raise, дедуп) —
`gate_failure` (`stage_engine._handle_qg_failure_rollbacks`, откат на `development`), `merge_hold`
(`stage_engine._handle_merge_verify` HOLD-ветка), `transient_retry` (`launcher._finalize_transient`
на **исчерпании** бюджета ретраев, а не на каждом backoff), `deploy_degraded` (post-deploy
`DEGRADED → set_repo_freeze`, урок слоя-3 «деплой OK / прод сломан» ET-8 — `attribution="unknown"`,
классифицируется позже).
- **Дедуп (D4):** для `source="auto"` — один indexed-SELECT по `idx_lessons_wi_type`: дубль с тем же
`(work_item_id, lesson_type, stage)` в окне `lessons_dedup_window_s` (env, дефолт 3600с) → no-op.
`source="manual"` дедуп НЕ проходит (оператор/Стрим всегда пишут).
- **Эндпоинты (D5):** `GET /lessons` (read-only, фильтры `type`/`status`/`repo`/`work_item`/`limit`),
`POST /lessons` (ручная запись, `source="manual"`), `POST /lessons/{id}` (доклассификация/update);
read-only ключ `lessons` в `GET /queue`. Выключенный флаг → `{"enabled": false}`.
- **never-raise (NFR-1):** все публичные функции и врезки изолированы (`try/except` → warning +
безопасный дефолт) — сбой журнала не роняет конвейер. Self-hosting-безопасно: только читает/пишет
свою таблицу, не деплоит/не рестартит прод/не трогает `main`/без процессов/сети. Детали —
`docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md`,
`docs/architecture/adr/adr-0034-lessons-journal.md`.
## Turnkey-онбординг проектов (ORCH-009)
Операторская способность развернуть **новый** проект одним проходом — **вне рантайма и вне
конвейера** (`src/**` байт-в-байт, kill-switch не нужен: активация — только явный запуск CLI
человеком). Три артефакта: **kit** `onboarding/repo-skeleton/` (параметризуемый каркас нового репо:
6 промптов канона 52d/92 — 5 ru + deployer en, паспорт `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`,
скелет `docs/` с обязательным `operations/INFRA.md`; плейсхолдеры `{{NAME}}`, словарь —
`onboarding/placeholders.json`; **канон не форкается**: `docs/_templates/`+`docs/_standards/`
копируются live из чекаута в момент материализации); **CLI** `scripts/onboard_project.py`
(`plan` — дефолт, GET-only / `apply` — идемпотентный ensure без delete / `verify`): Plane-проект +
22 статуса с точными именами (read-only импорт `plane_sync._PLANE_NAME_TO_KEY`; группы фиксированы
ADR: `STOP`→`cancelled`, терминальные группы только Done/Cancelled/STOP) + лейблы
`autoApprove`/`autoDeploy`/`Bug` → Gitea-репо + per-repo webhook (переиспользует глобальный
`ORCH_GITEA_WEBHOOK_SECRET`) → материализация kit + initial push **только** в свежесозданный пустой
репо → merged-вывод `ORCH_PROJECTS_JSON` (round-trip через фактический `_parse_projects_json`);
скрипт никогда не рестартит прод / не правит `.env` / ничего не удаляет; недоступное в Plane CE
API → `manual-step` (fail-safe); **runbook** `docs/operations/ONBOARDING.md` (ручные шаги: env +
управляемый рестарт; smoke — на staging 8501). Анти-дрейф — структурные тесты
`tests/test_onboarding_{kit,script,invariants}.py`. Детали —
`docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md`, сквозной
`docs/architecture/adr/adr-0035-turnkey-project-onboarding.md`.
## Конвенции
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
- Work items: `docs/work-items/<plane-id>/`
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза. **ORCH-52c (ORCH-076):** парсинг frontmatter сведён к единому контракту `src/frontmatter.py` (reader `read_frontmatter_value` — BC; единый парс-примитив `parse_frontmatter`; writer `render/write_frontmatter`; валидатор схемы `validate_schema`/`REQUIRED_FIELDS` — warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`). Пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) читают через ОДНУ точку парсинга; семантика вердиктов и `STAGE_TRANSITIONS`/состав `QG_CHECKS` — 1:1. Формальная спека «стадия → обязательный выход» + обязательная frontmatter-схема — `docs/_standards/HANDOFF_PROTOCOL.md`
## Артефакты задачи (`docs/work-items/<plane-id>/`)
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022), `18-coverage-report.md` (coverage-гейт: `coverage_status:`/measured/baseline, ORCH-027).
**Стандарт документов (ORCH-075, ORCH-52b):** структура каждого дока, карта «стадия→агент→документ→гейт→machine-key» и конвенция ADR-naming зафиксированы в `docs/_standards/PIPELINE_DOCS.md` (golden source); копируемые скелеты — в `docs/_templates/`. Перед написанием номерного дока бери скелет из `docs/_templates/` и не меняй имя machine-key frontmatter (регистр чувствителен — иначе гейт упадёт ложно).
## Правила для агентов
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR (формат — `docs/_standards/PIPELINE_DOCS.md` §4). Структура номерных доков и шаблоны — `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/`. Обнови `CHANGELOG.md`.
3. Никогда не править артефакты других этапов.
4. Никогда не комментировать ТЗ задним числом — если ТЗ не годится, возвращай в Анализ.
5. Никогда не закрывать задачу самостоятельно — это делает CI / финальная стадия.
6. **Reviewer проверяет: обновлена ли документация. Нет → REQUEST_CHANGES.** Это включает **обзорные доки** (ORCH-079, 52f — финал эпика 52): если PR закрывает пункт `README.md` «Известные ограничения», но README не обновлён → finding ≥P1 (витрина проекта не должна выдавать решённое за открытое).
7. Не использовать `--no-verify` без явного одобрения Owner.
8. Секреты — только в `.env`/`.env.staging` на хосте, в гит НЕ коммитятся (канон — `.env.example`).
9. **Трассировка маркеров (ORCH-078, ORCH-52e):** правишь строку/блок с маркером `ORCH-NNN` →
ПЕРЕД изменением прочитай его `docs/work-items/ORCH-NNN/06-adr/` и не сломай зафиксированный
инвариант; блок с 3+ маркерами → опирайся на сводный сквозной ADR. Стандарт маркеров (формат,
размещение, fallback-доступ, анти-археология, каноничное правило чтения) — `docs/_standards/TRACEABILITY.md`.
## ⚠️ Self-hosting — оркестратор правит САМ СЕБЯ
Задачи проекта ORCH меняют инструмент, который СЕЙЧАС работает в продакшене и обслуживает ДРУГИЕ проекты (enduro-trails) из ОДНОГО инстанса с ОБЩЕЙ БД и общей очередью.
- **НЕ перезапускать / не ронять прод-контейнер** `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

@@ -1,51 +1,11 @@
FROM python:3.12-slim
# ORCH-058 (Strategy B): stamp the image with the git commit it was built from so
# the deploy hook can fail-close if a stale staging image would be promoted to prod
# (INV-FRESH). Passed at build time via `--build-arg GIT_SHA=<sha>` (the staging
# rebuild in check_staging_image_fresh / the --build-staging hook mode supplies it).
# Without the build-arg the label is empty -> the hook treats it as a mismatch
# (fail-closed). The OCI-standard key is read by `docker image inspect`.
ARG GIT_SHA=""
LABEL org.opencontainers.image.revision=$GIT_SHA
WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq openssh-client git curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN apt-get update -qq && apt-get install -y -qq openssh-client git && rm -rf /var/lib/apt/lists/*
# git operations run as root over bind-mounted /repos (may be owned by host uid) -> trust it.
RUN git config --system --add safe.directory '*'
# ORCH-022: pinned gitleaks static Go binary for the offline secret-scan sub-gate
# (07-infra I-1). Baked into the image (NOT a pip package): the gate runs INSIDE the
# orchestrator container over a per-task worktree. Pinned release => deterministic
# rules; gitleaks needs no network so the "a secret always blocks" guarantee (BR-2)
# is independent of internet access. Multi-arch aware (amd64/arm64).
ARG GITLEAKS_VERSION=8.18.4
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) gl_arch="x64" ;; \
arm64) gl_arch="arm64" ;; \
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /tmp/gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${gl_arch}.tar.gz"; \
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks; \
chmod +x /usr/local/bin/gitleaks; \
rm -f /tmp/gitleaks.tar.gz; \
gitleaks version
# ORCH-58: compose runs the container as uid:gid 1000:1000 (ORCH-40), but the base
# image has no passwd entry for uid 1000 -> ssh/whoami fail with
# "No user exists for uid 1000" (rc=255), breaking the detached self-deploy ssh
# launch (ORCH-36 Phase B). Create a real user 1000 with a home dir so getpwuid()
# resolves and ssh can start.
RUN groupadd -g 1000 app && useradd -u 1000 -g 1000 -m -d /home/slin -s /bin/bash slin
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
# ORCH-021: do NOT `COPY data/ ./data/`. `data/` is gitignored (SQLite DB dir) and
# is provided at runtime as a bind-mount volume (`./data:/app/data`, see
# docker-compose.yml) which shadows anything baked into the image — so the COPY was
# dead weight. Worse, the ORCH-058 staging rebuild (`check_staging_image_fresh`)
# builds with the task *worktree* as the docker build context; a fresh worktree never
# contains the untracked `data/`, so `COPY data/` failed `docker build` with exit 1
# and bounced the task off `deploy-staging`. We just ensure the mountpoint exists.
RUN mkdir -p /app/data
COPY data/ ./data/
ENV PYTHONPATH=/app
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8500"]

130
README.md
View File

@@ -1,7 +1,5 @@
# Multi-Agent Orchestrator
> См. [CLAUDE.md](CLAUDE.md) (паспорт проекта) и [docs/architecture/README.md](docs/architecture/README.md) (архитектура).
FastAPI-сервис для оркестрации мульти-агентного пайплайна разработки. Принимает webhooks от Plane и Gitea, управляет жизненным циклом задач через Quality Gates, запускает Claude CLI агентов на каждой стадии.
## Архитектура
@@ -19,9 +17,9 @@ Gitea (git events) ─webhook──┘ │
## Стадии пайплайна
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
───── REQUEST_CHANGES ─────┘ (max 3 retries)
created → analysis → architecture → development → review → testing → deploy → done
└─── REQUEST_CHANGES ─┘ (max 3 retries)
```
| Стадия | Агент | Quality Gate (выход) | Триггер перехода |
@@ -29,11 +27,10 @@ created → analysis → architecture → development → review → testing →
| created | — | — | Plane webhook (work_item.created) |
| analysis | analyst | Файлы BRD/TRZ/AC/TestPlan | Push docs/ |
| architecture | architect | ADR или infra-requirements | Push docs/ |
| development | developer | check_ci_green (Gitea CI зелёный на ветке) | Auto-advance после developer |
| development | developer | check_tests_local (орк сам гоняет `make test`) | Auto-advance после developer |
| review | reviewer | check_reviewer_verdict (`verdict:` во frontmatter 12-review.md) | Auto-advance после reviewer |
| testing | tester | check_tests_passed (test-report.md) | Auto-advance после tester |
| deploy-staging | deployer | check_staging_status (15-staging-log.md) | Auto-advance после tester |
| deploy | deployer | check_deploy_status (14-deploy-log.md) | Auto-advance после staging |
| testing | tester | Test report с PASS | Auto-advance после tester |
| deploy | deployer | — | SSH deploy-hook |
| done | — | — | — |
## API Endpoints
@@ -45,7 +42,6 @@ created → analysis → architecture → development → review → testing →
| GET | `/queue` | Очередь задач (ORCH-1): counts по статусам + max_concurrency + последние 10 jobs |
| POST | `/webhook/plane` | Plane webhook receiver |
| POST | `/webhook/gitea` | Gitea webhook receiver |
| POST | `/bug-fast-track/escalate?work_item=<id>` | Эскалация багфикс-задачи в полный цикл (ORCH-019): сброс `track` `'bug'→'full'` → следующий переход уходит в `architecture` |
## Структура проекта
@@ -69,19 +65,10 @@ data/
├── orchestrator.db # SQLite database
└── runs/ # Agent output logs ({run_id}.log)
docs/
├── PRODUCT_VISION.md # Видение продукта
├── architecture/
│ ├── README.md # Обзор архитектуры, компоненты, API
│ ├── internals.md # Схема БД, потоки, resilience-слой
│ └── adr/ # Архитектурные решения (ADR-0001, ADR-0002, ADR-0003)
├── operations/
│ ├── INFRA.md # Топология, порты, env, self-hosting риски
│ ├── DEPLOY_HOOK.md # Деплой-хук
│ ├── STAGING.md # Staging-окружение
│ ├── STAGING_CHECK.md # Проверки staging
│ └── SETUP_WEBHOOKS.md # Настройка webhooks
├── work-items/ # Артефакты задач (00-15-*)
└── history/ # Исторические записи (BUGFIXES, INCIDENTS, ADR-архив)
├── ARCHITECTURE.md # Подробная архитектура
├── LESSONS_ET006.md # Lessons learned из ET-006
├── BUGFIXES_2026-05-21.md # Багфиксы
└── SETUP_WEBHOOKS.md # Настройка webhooks
docker-compose.yml # Deployment config
Dockerfile # Python 3.12 + Docker CLI + tini
```
@@ -122,7 +109,6 @@ uvicorn src.main:app --reload --port 8500
| `ORCH_REPOS_DIR` | Repos dir (container) | `/repos` |
| `ORCH_HOST_REPOS_DIR` | Repos dir (host) | `/home/slin/repos` |
| `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` |
| `ORCH_RUNS_DIR` | Базовый каталог per-run логов агентов (`<runs_dir>/{run_id}.log`, ORCH-087) | `/app/data/runs` |
| `ORCH_MAX_CONCURRENCY` | Сколько jobs воркер запускает параллельно (ORCH-1) | `1` |
| `ORCH_QUEUE_POLL_INTERVAL` | Период опроса очереди воркером, сек (ORCH-1) | `2.0` |
| `ORCH_PREFLIGHT_CACHE_TTL` | Кэш preflight (CLI/net), сек (ORCH-1 resilience) | `45` |
@@ -131,19 +117,6 @@ uvicorn src.main:app --reload --port 8500
| `ORCH_TRANSIENT_MAX_ATTEMPTS` | Ретраи для 429/недоступности | `5` |
| `ORCH_BREAKER_THRESHOLD` | transient подряд до открытия breaker | `3` |
| `ORCH_BREAKER_PAUSE_SECONDS` | Пауза при открытом breaker | `300` |
| `ORCH_RECONCILE_ENABLED` | Kill-switch sweeper потерянных webhook (ORCH-053) | `true` |
| `ORCH_RECONCILE_PLANE_ENABLED` | Отдельный флаг F-2 (опрос Plane API) | `true` |
| `ORCH_RECONCILE_INTERVAL_S` | Период фонового прохода reconciler, сек | `120` |
| `ORCH_RECONCILE_GRACE_DEFAULT_S` | Порог «застряла» по `tasks.updated_at`, сек | `600` |
| `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_STOP_STATUS_ENABLED` | Kill-switch отмены задачи по Plane-статусу **STOP** + закрытия дыры релонча (ORCH-090); `false` → поведение 1:1 как до ORCH-090 | `true` |
| `ORCH_STOP_STATUS_REPOS` | CSV область репо для STOP-отмены; пусто = все репо (ORCH-090) | `""` |
| `ORCH_BUG_FAST_TRACK_ENABLED` | Kill-switch багфикс-трека (ORCH-019): задача с меткой Plane `Bug` пропускает стадию `architecture`; `false` → старт и маршрут 1:1 как до ORCH-019 (нулевая регрессия) | `true` |
| `ORCH_BUG_FAST_TRACK_LABEL` | Имя метки Plane, активирующей багфикс-трек (ORCH-019) | `Bug` |
| `ORCH_BUG_FAST_TRACK_REPOS` | CSV область репо для багфикс-трека; **пусто → self-hosting only** (`orchestrator`) — enduro подключается явным CSV (ORCH-019) | `""` |
## Очередь задач (ORCH-1 / F-2b)
@@ -160,65 +133,12 @@ Webhook-хэндлеры больше не спавнят claude-агентов
- **Ретраи.** Упавший job (exit≠0) ретраится пока `attempts < max_attempts`,
потом `failed` + Telegram-нотификация.
Статусы job: `queued → running → done | failed`; **`cancelled`** — терминальный
исход STOP-отмены (ORCH-090), нигде не реквью'ится. Наблюдаемость — через `GET /queue`.
## Отмена задачи: статус STOP (ORCH-090)
Перевод задачи в выделенный Plane-статус **STOP** отменяет её: оркестратор
останавливает активного агента (graceful SIGTERM-каскад), снимает все job'ы
(терминальный `cancelled`, без авто-requeue), удаляет worktree и **рабочую**
ветку в Gitea (**никогда** `main`, без force-push), сбрасывает прогресс в
durable-терминал `tasks.stage='cancelled'` и тумбстонит натуральные ключи
(`#cancelled-<id>`), чтобы повторный «To Analyse» создал задачу **с нуля**.
Docs-артефакты (`01..17`) сохраняются. STOP во время критичного шага merge/deploy
**откладывается** до его честного завершения (никакого half-merge / рестарта
прода). Параллельно закрыта «дыра релонча»: ручной перевод в промежуточный рабочий
статус больше не релончит агента — единственный вход к запуску пайплайна остаётся
«To Analyse» (релонч агента сменой статуса разрешён только на стадии `analysis`
владельце Needs Input). Всё под kill-switch `ORCH_STOP_STATUS_ENABLED`, аддитивно,
never-raise. Наблюдаемость — блок `stop` в `GET /queue`. Деталь — `docs/work-items/
ORCH-090/06-adr/ADR-001-stop-cancel-task.md` + сквозной
`docs/architecture/adr/adr-0026-stop-cancel-task.md`.
> **Инфра-предусловие:** на доске Plane проекта ORCH создать статус **«STOP»** с
> группой `cancelled`. До создания статуса фича в fail-safe (нет UUID → ветка STOP
> не активируется).
## Багфикс-трек: дешёвый маршрут для багов (ORCH-019)
Задача с меткой Plane `Bug` (имя метки — `ORCH_BUG_FAST_TRACK_LABEL`, дефолт `Bug`)
идёт **укороченным маршрутом** конвейера: `analysis(lite) → development → review →
testing → deploy-staging → deploy → done`, т.е. **пропускается стадия `architecture`**
(отдельный прогон opus-агента `architect` + ADR + exit-гейт `check_architecture_done`).
Мини-аналитик выдаёт облегчённый пакет (короткий bug-report + обязательный план
регресс-теста), но всё равно все 4 файла analysis — гейт `check_analysis_complete`
не меняется.
**Корневой инвариант:** упрощается только аналитика/архитектура — **все Quality
Gate'ы и под-гейты исполняются без изменений** (`STAGE_TRANSITIONS` / `QG_CHECKS` /
`check_*` / machine-verdict ключи — байт-в-байт прежние). Маршрутизация багфикса —
свойство планировщика (routing-override в `advance_stage` по `tasks.track='bug'`),
**не** Quality Gate.
Классификация (`src/bug_fast_track.py`, never-raise): локальный `bug_fast_track_applies(repo)`
ПЕРВЫМ (выключенный флаг = нулевой сетевой оверхед), затем `is_bug_task` через
`labels.has_label` (источник истины — Plane API). Тип хранится в аддитивной колонке
`tasks.track` (`'full'` | `'bug'`), читается в горячем пути из БД (не из сети).
**Эскалация** сложного/архитектурного бага в полный цикл — `POST /bug-fast-track/escalate?work_item=<id>`
(сброс `'bug'→'full'`). Всё под kill-switch `ORCH_BUG_FAST_TRACK_ENABLED`, область —
`ORCH_BUG_FAST_TRACK_REPOS` (пусто → self-hosting only), fail-safe → полный цикл.
Наблюдаемость — блок `bug_fast_track` в `GET /queue` + отметка `🐞` в Telegram-карточке.
Деталь — `docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md` + сквозной
`docs/architecture/adr/adr-0032-bug-fast-track.md`.
> **Инфра-предусловие:** на доске Plane проекта ORCH создать метку **`Bug`**. До её
> создания фича в fail-safe (нет метки → задача идёт полным циклом).
Статусы job: `queued → running → done | failed`. Наблюдаемость — через `GET /queue`.
**Resilience-слой:** дешёвый preflight (CLI/net, кэш, без токенов) гейтит claim;
429/overload детектится по логу (transient vs permanent), transient ретраится с
exp-backoff (`available_at`, Retry-After); circuit breaker паузит воркер после N
transient подряд. Подробности: `docs/history/ORCH-1_JOB_QUEUE.md`.
transient подряд. Подробности: `docs/ORCH-1_JOB_QUEUE.md`.
## Multi-repo: реестр проектов (ORCH-6)
@@ -256,7 +176,7 @@ Plane-проект из маппинга.
docker exec orchestrator python3 -c "from src.projects import get_project_by_plane_id as g; print(g('<новый-uuid>'))"
```
Поля `name` опционально (по умолчанию = `repo`). Подробности — `docs/architecture/internals.md`.
Поля `name` опционально (по умолчанию = `repo`). Подробности — `docs/ARCHITECTURE.md`.
## Ключевые механизмы
@@ -282,7 +202,7 @@ stdout/stderr агента перенаправляются СРАЗУ в `/app/
Gitea events роутятся по типу:
- `push` → проверка файлов, advance architecture/development
- `pull_request*` (wildcard) → review approved/rejected, PR merge
- `status` → Gitea CI статус; ORCH-045: авторитетный гейт развития (`development → review`) — `check_ci_green` читает статус ветки с polling-retry (устраняет гонку «pending сразу после push»)
- `status` → (legacy) Gitea CI; С-1: больше не authoritative, `failure` логируется на debug и не блокирует/не алертит (QG развития = локальный `check_tests_local`)
## Тесты
@@ -292,19 +212,9 @@ pytest tests/ -v
## Известные ограничения
Реально открытые ограничения (сверено с кодом, ORCH-079):
1. **Telegram 48h** — карточки-сироты старше 48 часов неудаляемы (лимит Telegram Bot API); зачистка сирот самозалечивает только свежие (ORCH-087).
2. **Зависимости задач — только intra-repo (v1)** — `job_deps` выражают связи в пределах одного репозитория; кросс-репо зависимости пока не поддержаны (ORCH-026).
3. **Пакетный автоном — Этап 1** — per-repo serial gate сериализует задачи одного репо (ORCH-088); полный пакетный автономный прогон «1020 задач за ночь» — в развитии (эпик ORCH-088).
### Закрыто (история)
Пункты, ранее значившиеся ограничениями, закрыты кодом — оставлены как трассировка:
- **Single-task / shared `/repos` checkout** → git worktree per task (`ensure_worktree`) + serial-gate (ORCH-088) + task-deps (ORCH-026).
- **In-process daemon-потоки** → персистентная очередь задач (SQLite `jobs`, `src/queue_worker.py`), restart-safe (ORCH-1).
- **Gitea CI не настроен** → активный гейт стадии `development` — `check_ci_green` (`src/qg/checks.py`); `check_tests_local` помечен DEPRECATED.
- **No retry on API errors** → exp-backoff + circuit breaker в `queue_worker.py` (`ORCH_BACKOFF_*` / `ORCH_BREAKER_*` / `ORCH_TRANSIENT_MAX_ATTEMPTS`) + retry-loop в `check_ci_green` (ORCH-1 resilience / ORCH-045).
- **Plane sync — маппинг issue ID** → зрелый `src/plane_sync.py` (`find_issue_id`, `fetch_issue_sequence_id`) со статус-моделью и TTL-самозалечиванием (ORCH-010 / 066 / 068).
- **Tester timeout — Playwright e2e** → orchestrator является pytest-сервисом (Playwright неприменим); реальный механизм — конфигурируемый watchdog (`agent_timeout_seconds`, ORCH-7).
1. **Single-task / shared `/repos` checkout** — одновременно безопасно обрабатывается одна задача: все агенты и `check_tests_local` делают `git checkout` в одном `/repos/<repo>` → гонки при параллельных задачах. Исправление — git worktree per task (S-4, отдельно).
2. **Plane sync** — маппинг issue ID может быть некорректным (P3, в работе)
3. **In-process daemon-потоки**агенты живут в потоках uvicorn; при рестарте ловит orphan-recovery. Целевое — очередь задач (F-2b)
4. **Gitea CI не настроен**тесты гоняет сам оркестратор локально
3. **Tester timeout**e2e тесты с Playwright могут занимать >25 мин на тяжёлых фичах
4. **No retry on API errors** — httpx вызовы к Gitea/Plane без retry logic

View File

@@ -3,11 +3,6 @@ services:
build: .
container_name: orchestrator
restart: unless-stopped
# ORCH-040: бежим под uid:gid хоста (slin=1000:1000), а не root, чтобы
# артефакты конвейера (worktree + docs) создавались как slin:slin и git на
# хосте работал без ручного chown. Доступ к docker.sock сохранён через
# group_add: ["999"] (МИНА 1 — НЕ удалять). См. ADR-001 ORCH-040.
user: "1000:1000"
# init: true injects docker-init (tini) as PID 1 so reparented grandchild
# processes from the claude/node subprocess tree are reaped (no zombies, B-2).
init: true
@@ -20,92 +15,13 @@ services:
- /usr/bin/node:/usr/bin/node:ro
- /home/slin/.claude:/home/slin/.claude
- /home/slin/.claude.json:/home/slin/.claude.json:ro
# ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh.
- /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro
- /home/slin/.orchestrator-ssh:/root/.ssh:ro
env_file: .env
environment:
- ORCH_REPOS_DIR=/repos
- ORCH_HOST_REPOS_DIR=/home/slin/repos
# legacy enduro deployer (read via os.environ, keep as-is):
- DEPLOY_SSH_USER=slin
- DEPLOY_SSH_HOST=127.0.0.1
- DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh
# ORCH-036 self-deploy (read via pydantic ORCH_ prefix; host-network -> 127.0.0.1, ssh key mounted):
- ORCH_DEPLOY_SSH_USER=slin
- ORCH_DEPLOY_SSH_HOST=127.0.0.1
- ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh
- ORCH_DEPLOY_HOST_REPO_PATH=/home/slin/repos/orchestrator
group_add:
- "999"
# ORCH-100 (FND/F1b): sidecar-watchdog — the monitoring brain in a SEPARATE
# container (observer separated from observed, ADR-001 D2). Deploying it builds
# ONLY this service — the prod `orchestrator` is NOT rebuilt/restarted.
# * network_mode: host -> /metrics reachable at http://127.0.0.1:8500/metrics
# and host interfaces visible for memory/disk reads.
# * docker.sock mounted :ro AND the code is GET-only (double read-only guard).
# * host disk paths bind-mounted :ro so shutil.disk_usage sees the host FS but
# can never write (opt-in disk ceiling, D6).
# * mem_limit caps the thin stdlib daemon (D2): OOM = early "sidecar grew" signal.
# * WATCHDOG_ENABLED=false (or simply not starting the service) -> inert.
orchestrator-watchdog:
build:
context: .
dockerfile: watchdog/Dockerfile
container_name: orchestrator-watchdog
restart: unless-stopped
init: true
network_mode: host
mem_limit: 128m
mem_reservation: 32m
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /home/slin/repos:/repos:ro
- ./data:/app/data:ro
# Optional env_file (required: false): a missing .env.watchdog must NOT fail
# `docker compose up` for the prod orchestrator (self-hosting safety). Absent
# file -> WATCHDOG_* defaults, no token -> fail-safe (logs, does not send).
env_file:
- path: .env.watchdog
required: false
group_add:
- "999"
# ORCH-31: staging instance (port 8501, isolated DB).
# Starts ONLY with: docker compose --profile staging up -d orchestrator-staging
# Normal "docker compose up -d" does NOT start this service.
orchestrator-staging:
profiles:
- staging
build: .
container_name: orchestrator-staging
restart: unless-stopped
# ORCH-040: тот же uid хоста, что и у prod (см. комментарий выше / ADR-001).
user: "1000:1000"
init: true
network_mode: host
command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8501"]
volumes:
- ./data/staging:/app/data
- /home/slin/repos:/repos
- /var/run/docker.sock:/var/run/docker.sock
- /usr/lib/node_modules/@anthropic-ai/claude-code:/opt/claude-code:ro
- /usr/bin/node:/usr/bin/node:ro
- /home/slin/.claude:/home/slin/.claude
- /home/slin/.claude.json:/home/slin/.claude.json:ro
# ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh.
- /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro
env_file: .env.staging
environment:
- ORCH_REPOS_DIR=/repos
- ORCH_HOST_REPOS_DIR=/home/slin/repos
- DEPLOY_SSH_USER=slin
- DEPLOY_SSH_HOST=127.0.0.1
- DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh
# Staging DB is isolated via ./data/staging volume mount.
# Inside the container the path remains /app/data/orchestrator.db (same default),
# but on the host it physically lives at ./data/staging/orchestrator.db —
# completely separate from prod ./data/orchestrator.db.
- ORCH_DB_PATH=/app/data/orchestrator.db
group_add:
- "999"

View File

@@ -58,18 +58,11 @@ STAGE_TRANSITIONS = {
architecture: → development (agent: developer, QG: check_architecture_done)
development: → review (agent: reviewer, QG: check_tests_local)
review: → testing (agent: tester, QG: check_reviewer_verdict)
testing: → deploy-staging (agent: deployer, QG: check_tests_passed)
deploy-staging: → deploy (agent: deployer, QG: check_staging_status)
testing: → deploy (agent: deployer, QG: check_tests_passed)
deploy: → done (agent: None, QG: None)
cancelled: → None (agent: None, QG: None) # ORCH-090: терминал-сток отмены
}
```
**Терминальные стоки (ORCH-090):** `done` и `cancelled` — равноправные терминальные состояния
(`{"next": None, "agent": None, "qg": None}`). `cancelled` — это **не новое ребро** (exit-гейты
рёбер не меняются), а терминал STOP-отмены. Системный предикат «задача завершена» —
`stage ∈ {done, cancelled}` (синхронно в `reconciler`/`serial_gate`/`task_deps`; adr-0026).
### 3. Quality Gates (`src/qg/checks.py`)
| Check | Метод проверки |
@@ -106,17 +99,6 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
Примечание: переход `review → testing` использует `check_reviewer_verdict` (читается из frontmatter `12-review.md`); `development → review``check_tests_local` (оркестратор сам прогоняет тесты, не зависит от Gitea CI).
**Багфикс-трек: routing-override на ребре выхода из `analysis` (ORCH-019 — design).** Для задачи
с `tasks.track='bug'` (помечена в `start_pipeline` по метке Plane `Bug` через аппарат ORCH-089)
`advance_stage` на шаге 3 переопределяет результат `get_next_stage('analysis')`: `next_stage`
`development` (вместо `architecture`), а на шаге 4 `next_agent``developer` (вместо `architect`)
→ стадия `architecture` и её exit-гейт `check_architecture_done` для багфикса не исполняются.
`STAGE_TRANSITIONS`/`get_next_stage`/`get_agent_for_stage` остаются чистыми (1:1) — override живёт
только в `advance_stage`. Чистый предикат `bug_fast_track.skips_architecture(track)` (leaf
`src/bug_fast_track.py`, never-raise) под `bug_fast_track_enabled`; `track` читается из БД, не из
сети (NFR-4). `False`/неприменимый репо → маршрут байт-в-байт прежний. Детали —
[adr-0032](adr/adr-0032-bug-fast-track.md).
### 6. Review Bounce
При REQUEST_CHANGES:
@@ -124,37 +106,6 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
2. Если < MAX_DEV_RETRIES (3) — откатывает в development, перезапускает developer
3. Если >= MAX_DEV_RETRIES — эскалация (логирование + уведомление)
### 7. Live Telegram tracker (`src/notifications.py`)
Вместо ~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`; дефолт переключён `edit → bump` в ORCH-067).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` → безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
| Режим | Поведение при обновлении |
|-------|--------------------------|
| `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`;
- `ok:false` с маркерами `_DELETE_GONE_MARKERS` (`message to delete not found`, `message can't be deleted`, `message_id_invalid`) → `True` (старше 48ч / уже удалено — не транзиент);
- прочий `ok:false` / 5xx / исключение (сеть/таймаут) → `False` + `logger.warning`;
- нет токена/chat_id → `False`, HTTP не выполняется.
Результат `delete_telegram` **не** блокирует отправку новой карточки (BR-6: delete-fail у сообщения >48ч → всё равно шлём новое); `False` означает лишь «старое, возможно, ещё живо» — будет вычищено повторной попыткой на следующем переходе. При транзиентном сбое send (`None`) указатель `tracker_message_id` **не** затирается (анти-затирание, симметрично edit-fallback).
**Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются.
**Строки стадий: отражение откатов + суммирование метрик (ORCH-091).** Цикл рендера строк стадий (`render_task_tracker``_stage_line`) исправлен по двум осям. (1) **Откат (Деф.2):** `✅`-строка стадии рисуется только если её позиция в конвейере `≤` текущей позиции задачи; позиция берётся из порядка `STAGE_TRANSITIONS` (read-only хелпер `_pipeline_pos`, never-raise; неизвестная стадия → «далёкое будущее» → ✅ не пере-подавляется) с нормализацией `deploy-staging → deploy` ТОЛЬКО в гейте подавления (схлопнутая строка «Внедрение» несёт `stage_key="deploy"`). После отката (`deploy-staging → development`, `review → development`) строки стадий ПОЗЖЕ текущей больше не рисуются как пройденные — пропадает абсурд «✅ Внедрение + 🔄 Разработка»; `is_active_stage` не тронут. (2) **Метрики (Деф.3):** `_stage_line` агрегирует ВСЕ `agent_runs` агента стадии (Σ cost / Σ токены / Σ время теми же per-run-формулами, что блок тоталов задачи), а не последний прогон — каждый агент привязан ровно к одной строке `_TRACKER_STAGES`, поэтому Σ(строк стадий) ≡ тоталы ≡ `SUM(agent_runs)` по `task_id`; модель/эффорт/«попытка N» берутся из последнего прогона. Прогоны, подавлённые откатом, по-прежнему входят в тоталы (намеренная семантика отката).
**Строка 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-staging→Deploying (staging)` [ORCH-091], `deploy→⏸ Awaiting Deploy`, `done→Done`, `cancelled→Cancelled` [ORCH-091]) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). **ORCH-091:** карта `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (полнота — тестом, не статичным списком); неизвестная/будущая стадия → нейтральный фолбэк (капитализированное имя стадии), а НЕ «To Analyse» (он остаётся лишь явным лейблом `created` и безопасной деградацией на истинно-битом входе).
- **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`).
**HTML-безопасность данных карточки (ORCH-095).** Текст карточки шлётся с `parse_mode=HTML` и собирается из слотов двух категорий: **markup** (намеренная разметка — `num_html`/`plane_issue_link`, `link_for(...)`, `_done_link(...)`, уже-экранированный `esc_title`) и **data** (подставляемые значения — длительности `_fmt_minutes`/`_capped_review_str`, статус-лейбл `_card_status_label`, имя модели `short_model_name`, эффорт `_run_effort`, токены/стоимость `fmt_tokens`/`fmt_cost`). Инвариант: **каждый data-слот экранируется `html.escape` ровно один раз на границе рендера** (`render_task_tracker`/`_stage_line`); функции-источники остаются HTML-агностичными, markup-слоты не экранируются (двойное экранирование запрещено). Это устранило класс «неэкранированные данные в HTML-тексте»: до фикса `_fmt_minutes(<60s)` возвращал литерал `<1м`, который Telegram парсил как открывающий тег → `editMessageText` `400 can't parse entities``EDIT_FAILED` → ранний `return` (анти-дубль ORCH-087) → карточка застывала (инцидент ORCH-093). `_fmt_minutes` по-прежнему возвращает `<1м` — escape на границе (`&lt;1м`) рендерит его визуально идентично; формат не меняется. Застрявшая (в окне) карточка авто-восстанавливается следующим безопасным рендером; `edit_telegram`/`update_task_tracker`/леджер сирот/режимы `bump`/`edit` не тронуты. Детали — [ORCH-095 ADR-001](../work-items/ORCH-095/06-adr/ADR-001-html-safe-card-data-render.md).
## Database Schema
```sql
@@ -238,10 +189,8 @@ services:
12. Gitea PR webhook: review event → QG check_review_approved → PASS
13. Advance: review → testing, tester launched
14. Tester: прогоняет тесты, пишет test-report.md → git push
15. Auto-advance: testing → deploy-staging (QG check_tests_passed → PASS)
16. Deployer: runs staging checkswrites 15-staging-log.md (staging_status: SUCCESS)
17. Auto-advance: deploy-staging → deploy (QG check_staging_status → PASS)
18. PR merge → Gitea PR webhook: action=closed, merged=true → done
15. Auto-advance: testing → deploy (QG check_tests_passed → PASS)
16. PR mergeGitea PR webhook: action=closed, merged=true → done
```
### Review bounce path
@@ -350,10 +299,9 @@ webhook (plane/gitea) background thread (queue_worker)
| Колонка | Назначение |
|--------|------------|
| `status` | `queued``running``done` \| `failed` \| `cancelled` (ORCH-090: терминальный исход STOP-отмены, не реквью'ится) |
| `status` | `queued``running``done` \| `failed` |
| `attempts` / `max_attempts` | счётчик попыток (инкремент при claim) / лимит ретраев (default 2) |
| `run_id` | FK на `agent_runs.id` после старта |
| `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent) |
| `task_content` | ТЗ, которое пишется в task-файл агента |
| `error` | последняя ошибка |
@@ -371,44 +319,10 @@ status='queued'` и проверяет `rowcount`. При гонке двух т
jobs со статусом `running` (воркер умёр на рестарте) → возвращаются в `queued`.
Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
### Job-reaper (ORCH-065, рестарт НЕ требуется)
`requeue_running_jobs()` спасает ТОЛЬКО на старте процесса. Зомби-job, возникший
**без** рестарта (умер monitor-поток/дочерний процесс, а сервис жив), оставался
`running` навсегда и при `max_concurrency=1` блокировал всю очередь. Фоновый
daemon-поток `src/job_reaper.py` (каркас `reconciler`) периодически
(`reaper_interval_s`) сканирует `running`-jobs и реапит «мёртвые»:
- **Tier-1**`jobs.pid` мёртв (`os.kill(pid,0)``ProcessLookupError`) на
протяжении `reaper_dead_ticks` подряд тиков (анти-ложноположительность);
- **Tier-2**у `agent_runs[run_id]` записан `exit_code`, а `jobs.status` ещё
`running`. Окно неоднозначно: живой monitor пишет `exit_code` ПЕРВЫМ, затем
git push/PR/Plane-комментарии (секунды-десятки секунд) и лишь потом
`_finalize_job`; pid агента к этому моменту мёртв в обоих случаях. Поэтому
Tier-2 реапит только после finalization-grace `reaper_finalize_grace_s`
(`finished_age_s >= grace`) — живой финализирующий monitor НЕ реапится;
- **Tier-3** — backstop: job висит `running` дольше `reaper_max_running_s`.
Реап атомарен (`UPDATE jobs SET ... WHERE id=? AND status='running'` + `rowcount`,
как `claim_next_job`) → совместим со стартовым `requeue_running_jobs` без двойной
обработки. Действие — **claim-before-act**: для exit0 канонический QG оценивается
read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и только победитель
claim делает `_try_advance_stage` (advance+enqueue) — проигравший (поздний monitor
/ стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
источник истины — QG, не «exit0»; гейт красный или exit≠0/неизвестно →
`attempts<max``queued`, иначе `failed`+Telegram. Тот же поток на старте и
периодически делает проактивный реклейм stale/dead merge-lease (`merge_gate.py`:
`pid_alive`/`reclaim_stale_lease`). never-raise; kill-switch `ORCH_REAPER_ENABLED`
/ `ORCH_LEASE_RECLAIM_ENABLED`; снимок в `GET /queue` (блок `reaper`). Подробнее —
adr-0011.
### Конфиг
- `ORCH_MAX_CONCURRENCY` (default 1) — лимит параллельных jobs.
- `ORCH_QUEUE_POLL_INTERVAL` (default 2.0) — период опроса.
- `ORCH_AGENT_MODEL_DEFAULT` / `ORCH_AGENT_MODEL_<AGENT>` (ORCH-41) — модель агентов; дефолт `claude-opus-4-8`.
- `ORCH_AGENT_EFFORT_DEFAULT` / `ORCH_AGENT_EFFORT_<AGENT>` (ORCH-41) — режим `--effort` (low|medium|high|xhigh|max).
- `ORCH_AGENT_FALLBACK_MODEL` (ORCH-41) — опц. `--fallback-model` при overloaded.
- per-project override: `agent_models` / `agent_efforts` в `ORCH_PROJECTS_JSON`; резолверы `resolve_agent_model` / `resolve_agent_effort` (project > per-agent env > default > пусто).
Наблюдаемость: `GET /queue` — counts по статусам + последние 10 jobs.

View File

@@ -12,36 +12,30 @@ Internal URL: `http://127.0.0.1:8500/`
---
## Gitea Webhook (per-repo)
## Gitea Webhook
Gitea-webhook — **per-repo**: создаётся для КАЖДОГО подключаемого к оркестратору репозитория
(`<repo>` ниже). Для новых проектов его создаёт onboarding-CLI
(`scripts/onboard_project.py apply`) — полный процесс см. `docs/operations/ONBOARDING.md`;
команды ниже — для ручной проверки/пересоздания на любом репо.
**Создан автоматически через API.**
- URL: `https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea`
- Events: `push`, `pull_request`, `status`
- Secret: значение `ORCH_GITEA_WEBHOOK_SECRET` в `.env` — **ОДИН глобальный секрет на все
репо** (приёмник валидирует только его; новый секрет на одном репо сломал бы HMAC остальных —
при ротации меняется на всех репо разом)
- Secret: значение `ORCH_GITEA_WEBHOOK_SECRET` в `.env`
- Signature header: `X-Gitea-Signature` (HMAC-SHA256 hex digest)
### Проверка
```bash
GITEA_TOKEN=$(grep ORCH_GITEA_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)
curl -s "http://localhost:3000/api/v1/repos/<owner>/<repo>/hooks" \
curl -s "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -m json.tool
```
### Пересоздание (если нужно)
```bash
# Секрет переиспользуй из .env (ORCH_GITEA_WEBHOOK_SECRET); генерируй новый ТОЛЬКО при
# первичной настройке/осознанной ротации (и обнови вебхуки ВСЕХ репо):
GITEA_WEBHOOK_SECRET=$(grep ORCH_GITEA_WEBHOOK_SECRET /home/slin/repos/orchestrator/.env | cut -d= -f2)
GITEA_WEBHOOK_SECRET=$(openssl rand -hex 20)
# Обновить в .env: ORCH_GITEA_WEBHOOK_SECRET=<new_secret>
curl -X POST "http://localhost:3000/api/v1/repos/<owner>/<repo>/hooks" \
curl -X POST "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{

View File

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

View File

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

View File

@@ -1,147 +0,0 @@
# TRACEABILITY — стандарт маркеров-трассировки `ORCH-NNN` (golden source трассировки)
> **Назначение.** Единый нормативный контракт: как нетривиальная строка/блок/инвариант в коде
> привязывается к work item, который его ввёл, и к его архитектурному решению (ADR). Это **слой 4
> (трассировка)** эпика **ORCH-52** — рядом с `PIPELINE_DOCS.md` (слой 1, структура документов) и
> `HANDOFF_PROTOCOL.md` (слой 2, машинный frontmatter-контракт).
>
> **Статус истины.** Документ **кодифицирует сложившуюся практику**, а не вводит новый синтаксис.
> Источник истины о *поведении* остаётся код (`src/stages.py`, `src/qg/checks.py`,
> `src/stage_engine.py`); этот стандарт — описательно-нормативный, **не машинный гейт конвейера**.
> Соблюдение держится на дисциплине агентов + оси ревью (`reviewer.md`), а не на CI-lint.
Введён задачей **ORCH-078** (ORCH-52e). Сквозной ADR:
[`docs/architecture/adr/adr-0022-traceability-marker-standard.md`](../architecture/adr/adr-0022-traceability-marker-standard.md);
детально — `docs/work-items/ORCH-078/06-adr/ADR-001-traceability-marker-standard.md`. Продолжает
цепочку стандартов эпика 52: adr-0019 (52b), adr-0020 (52c), adr-0021 (52d).
---
## 1. Назначение и определение
**Маркер `ORCH-NNN`** (а для проекта enduro-trails — `ET-NNN`) в коде = обязательный стандарт
трассировки: он привязывает нетривиальную строку / блок / инвариант к work item, который его ввёл,
и к его ADR. Это даёт читающему агенту прямой путь «строка кода → решение, которое её породило»,
вместо `git blame`-археологии.
**Факт (сверено на 2026-06-09):** в `src/` де-факто живёт **51 уникальный** маркер `ORCH-NNN`
(`grep -rhoE 'ORCH-[0-9]+' src/ | sort -u | wc -l``51`) — сложившаяся практика. Этот стандарт её
формализует. **Массовый ретро-фит существующих 51 маркера вне объёма** — стандарт нормативен «на
будущее»: его правила применяются к **новому и правимому** коду.
---
## 2. Формат маркера
Маркер — это **inline-комментарий** (или фрагмент docstring модуля/функции), содержащий идентификатор
work item `ORCH-NNN`. Рекомендуется рядом указывать ссылку на конкретное решение в ADR, чтобы трасса
вела не просто к задаче, а к пункту решения:
```python
# Ordering term — ``t2.id < jobs.task_id`` (FIFO, ORCH-088, ADR-001 D1 / FR-2): a task
# does not enter `analysis` while an earlier unfinished task exists in the same repo.
```
Нового синтаксиса не вводится — кодифицируется уже сложившийся стиль (`ORCH-NNN[, ADR-001 D1]`).
---
## 3. Где ставится маркер
Маркер ставится рядом с **нетривиальным инвариантом**, понимание которого требует контекста решения:
- выбор fail-open / fail-closed поведения;
- точное условие сериализации / упорядочивания (FIFO, lease, барьер);
- идемпотентность / защита от повторной обработки;
- обходимая «дыра» конвейера, которую блок закрывает;
- любое условие, чьё «почему именно так» зафиксировано в ADR.
Маркер **НЕ ставится** на тривиальном/самоочевидном коде (геттеры, простые присваивания, очевидные
проверки) — это только зашумляет.
**Правило для нового кода:** вводишь значимый инвариант → ставь маркер своей задачи (`ORCH-NNN`)
рядом, по возможности со ссылкой на пункт ADR.
---
## 4. Как читать историю (с реальным проверяемым примером)
Пошагово, от строки кода к решению:
1. Видишь в коде маркер `ORCH-NNN` у строки/блока, который собираешься менять.
2. Открываешь его архитектурное решение: `docs/work-items/ORCH-NNN/06-adr/`.
3. Читаешь зафиксированный инвариант ПЕРЕД правкой; не ломаешь его (см. §7).
**Проверяемый пример из реального кода (`main`):**
> `src/serial_gate.py` несёт условие сериализации `t2.id < jobs.task_id` с маркером **ORCH-088**
> и отсылкой `ADR-001 D1 / FR-2` (FIFO-уточнение serial-gate). Чтобы понять, почему задача не входит
> в `analysis`, пока в репо есть более ранняя незавершённая задача, читаешь:
> `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`.
Пример ссылается на **реально существующие** в `main` файл и ADR — иначе стандарт опровергал бы сам
себя (нерабочая трассировка).
---
## 5. Fallback-доступ к чужому ADR
Папки `docs/work-items/ORCH-NNN/` может **не быть в текущей ветке** (она срезана от `main` без неё —
типично для ветки другой задачи). Штатный способ прочитать чужой ADR — взять его из `origin/main`:
```bash
git fetch origin # при необходимости заранее
git ls-tree origin/main:docs/work-items/ORCH-NNN/06-adr/ # листинг доступных ADR
git show origin/main:docs/work-items/ORCH-NNN/06-adr/ADR-001-<slug>.md # прочитать конкретный
```
Это не блокер: отсутствие папки в ветке ≠ отсутствие решения — оно всегда есть в `main`.
---
## 6. Анти-археология: 3+ маркеров → сводный сквозной ADR
Если функция/блок несёт **3+** маркеров `ORCH-NNN` (эволюционировал через много задач), раскопки по
каждому work item нечитаемы. Вместо перечисления всех задач ставится **одна сводная ссылка на
сквозной ADR** (`docs/architecture/adr/adr-NNNN-*`), агрегирующий эволюцию.
Числовой порог `3` — граница, за которой inline-перечисление перестаёт быть читаемым (один-два
маркера ещё информативны, три и больше — уже археология).
**Пример из кода:** `src/merge_gate.py` несёт маркеры ORCH-043/065/071/073 (и ещё несколько) →
читать сводные сквозные `adr-0006` (merge-gate), `adr-0013` (merge-verify-gate),
`adr-0014` (sha-source-of-truth), `adr-0016` (ensure-open-PR) в `docs/architecture/adr/`, а не 8
отдельных work item.
Это конвенция для **нового/правимого** блока; массовая переразметка существующих файлов вне объёма.
---
## 7. Правило чтения (каноничная формулировка — единый источник)
Это **единственное** место, где живёт каноничный текст правила. Промпты агентов
(`developer.md`/`architect.md`/`reviewer.md`) **ссылаются** на него, а не копируют — чтобы не было
дрейфа формулировок между файлами.
> **Правишь код с маркером `ORCH-NNN` → прочитай его `docs/work-items/ORCH-NNN/06-adr/` ПЕРЕД
> изменением; не сломай зафиксированный инвариант. Не можешь сохранить инвариант — эскалируй /
> верни задачу в анализ, не правь вслепую.** Папки нет в ветке → читай из `origin/main` (§5). Блок
> несёт 3+ маркеров → опирайся на сводный сквозной ADR (§6).
Кто и как применяет правило:
- **developer / architect** — обязаны выполнить чтение ПЕРЕД правкой маркированного кода.
- **architect** — при введении/правке блока с 3+ маркерами оформляет/обновляет сводный сквозной ADR.
- **reviewer** — проверяет соблюдение: правка маркированного (`ORCH-NNN`) кода без сверки с его ADR
или со сломом инварианта → finding (рекомендуемая severity **P1**; слом критического инварианта
конвейера — на усмотрение reviewer вплоть до P0).
---
## Связи
- Сквозной ADR: [`adr-0022`](../architecture/adr/adr-0022-traceability-marker-standard.md).
- Стандарты-соседи: [`PIPELINE_DOCS.md`](PIPELINE_DOCS.md) (слой 1),
[`HANDOFF_PROTOCOL.md`](HANDOFF_PROTOCOL.md) (слой 2).
- Цепочка эпика 52: adr-0019 (52b) / adr-0020 (52c) / adr-0021 (52d) / adr-0022 (52e).
- Прецедент класса ошибки (слом инварианта без чтения ADR): `docs/history/LESSONS_2026-06-08_phantom-merge.md`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
---
coverage_status: PASS # PASS | FAIL (machine-key — читает check_coverage_gate)
work_item: ORCH-NNN
measured_coverage: 0.0 # измеренное line coverage src/ (%, float)
baseline: 0.0 # базовая линия main на момент измерения (%, или пусто при bootstrap)
floor: 0.0 # абсолютный порог coverage_min_percent (%)
policy: both # absolute | baseline | both
epsilon: 0.5 # допуск на шум измерения (%)
delta: 0.0 # measured max(baseline, floor) (%, знаковая дельта)
---
# Coverage Report — ORCH-NNN
> Детерминированный гейт покрытия (ORCH-027) — под-гейт ребра `deploy-staging→deploy` (врезка в
> `advance_stage`, ПОСЛЕ merge-gate, ДО image-freshness; не строка `STAGE_TRANSITIONS`). Машинный
> вердикт читается ТОЛЬКО из `coverage_status:`. `PASS` → дальше; `FAIL` → откат на `development`.
> Измерение — `pytest --cov=src --cov-report=json` в изолированном worktree. Source of truth
> измеренного значения для ratchet базовой линии (`_handle_merge_verify`, ребро `deploy→done`).
## Verdict
<PASS / FAIL: measured X% vs floor F% / baseline B% (policy=…, epsilon=…), delta=±D%.>
## Measurement
<Инструмент (pytest-cov/coverage.py), команда, line coverage src/ = X%; либо fail-open WARNING
при ошибке инструмента (coverage_tool_fail_closed=False).>
## Policy
<Режим (absolute|baseline|both), порог floor, базовая линия main, epsilon, какое условие
нарушено при FAIL.>

File diff suppressed because one or more lines are too long

View File

@@ -1,59 +0,0 @@
# Architecture Decision Records
Индекс сквозных (cross-cutting) ADR проекта orchestrator.
Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-slug.md`.
| # | Решение | Статус | Дата | Источник |
|---|---------|--------|------|----------|
| adr-0001 | Реестр проектов (multi-repo) | accepted | 2026-06-02 | ORCH-6 |
| adr-0002 | Очередь задач вместо in-process потоков | accepted | 2026-06-03 | ORCH-1 |
| adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 |
| adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 |
| adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 |
| adr-0006 | Merge-gate (догон main + re-test + сериализация слияний) | proposed | 2026-06-06 | ORCH-043 |
| adr-0007 | Reconciler застрявших стадий (sweeper потерянных webhook) | accepted | 2026-06-06 | ORCH-053 |
| adr-0007 | Исполняемый самодеплой стадии `deploy` (файл adr-0007-executable-self-deploy) | accepted | 2026-06-06 | ORCH-036 |
| adr-0008 | Провенанс staging-образа перед BUILD-ONCE retag | accepted | 2026-06-06 | ORCH-058 |
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
| 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 |
| adr-0016 | ensure_open_pr — гарантированный код-PR перед merge-verify | accepted | 2026-06-09 | ORCH-082 |
| adr-0017 | Per-repo serial gate (пакетный автономный режим, serial e2e) | proposed | 2026-06-09 | ORCH-088 |
| adr-0018 | Авто-режим по лейблам (autoApprove + autoDeploy) | accepted | 2026-06-09 | ORCH-089 |
| adr-0019 | Стандарт документов конвейера (PIPELINE_DOCS, слой 1) | accepted | 2026-06-09 | ORCH-075 |
| adr-0020 | Единый frontmatter-контракт + спека handoff (reader/writer/валидатор) | accepted | 2026-06-09 | ORCH-076 |
| adr-0021 | Канон Anthropic для агент-промптов + эмиссия frontmatter-схемы 52c | proposed | 2026-06-09 | ORCH-077 |
| adr-0022 | Стандарт трассировочных маркеров `ORCH-NNN` | accepted | 2026-06-09 | ORCH-078 |
| adr-0023 | Обзорная ось reviewer + закрытие эпика 52 | accepted | 2026-06-09 | ORCH-079 |
| adr-0024 | Disk-watchdog — heartbeat-сигнал заполнения хост-ФС | proposed | 2026-06-09 | ORCH-063 |
| adr-0025 | Build-cache-pruner — авто-prune docker build cache на хосте | proposed | 2026-06-09 | ORCH-062 |
| adr-0026 | STOP / отмена задачи — системный терминал `cancelled` | proposed | 2026-06-09 | ORCH-090 |
| adr-0027 | Merge-актор — ретрай транзиентных ошибок Gitea + гард «ветка уже в `main`» | proposed | 2026-06-09 | ORCH-093 |
| adr-0028 | Terminal-window-aware гард deploy-фазовых статусов Plane | proposed | 2026-06-09 | ORCH-094 |
| adr-0029 | Гейт покрытия тестами — edge sub-gate + ratchet-базовая линия | proposed | 2026-06-10 | ORCH-027 |
| adr-0030 | Лёгкий read-only `/metrics` — сырьё о самом орке для sidecar (F1b) | proposed | 2026-06-10 | ORCH-099 |
| adr-0031 | Нормализация legacy root-owned файлов при миграции uid — детект-leaf + actionable worktree-ошибка | proposed | 2026-06-10 | ORCH-057 |
| adr-0032 | Багфикс-трек — укороченный маршрут конвейера для багов | proposed | 2026-06-10 | ORCH-019 |
| adr-0033 | Sidecar-watchdog F1b — мозг мониторинга в отдельном контейнере | proposed | 2026-06-10 | ORCH-100 |
| adr-0034 | Машинный журнал уроков — таблица `lessons` + observer-leaf | proposed | 2026-06-10 | ORCH-098 |
| adr-0035 | Turnkey-онбординг проектов — kit + операторский CLI + runbook | proposed | 2026-06-10 | ORCH-009 |
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
> свободный номер (текущий максимум — `0035`).
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
> adr-0016 **amends** adr-0013/0014 (гарантирует открытый код-PR перед merge_pr, ORCH-082).
> adr-0020 реализует машинный слой к adr-0019 (ORCH-52b→52c).
> adr-0021 реализует слой промптов к adr-0019/0020 (ORCH-52d — замыкает эпик 52).
> adr-0025 **комплементарен** adr-0024 (watchdog сигналит о росте диска — pruner убирает
> доминирующего «пожирателя», docker build cache).
## Формат
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
Принятый ADR не меняется — новое решение заводится отдельным файлом со ссылкой `supersedes adr-XXXX`.
Новые ADR добавляет архитектор при принятии решения (см. `CLAUDE.md` → Конвенции).

View File

@@ -1,23 +0,0 @@
# adr-0001: Реестр проектов (multi-repo)
- **Статус:** accepted
- **Дата:** 2026-06-02
- **Задача:** ORCH-6
## Контекст
Инцидент 2026-06-02: Plane-вебхук слушал весь воркспейс и хардкодил `repo = settings.default_repo` (enduro-trails). Задачи ЛЮБОГО проекта сливались в один репо с одним префиксом (ET). Нужна изоляция по проектам.
## Решение
Введён реестр `src/projects.py`: `ProjectConfig` (frozen dataclass) связывает `plane_project_id``repo` + `work_item_prefix` + `name`. Источник правды — env `ORCH_PROJECTS_JSON`; при пустом/невалидном — встроенный дефолт (`enduro-trails`/ET, `orchestrator`/ORCH). Позволяет: фильтровать вебхуки по проекту (неизвестный → ignore), резолвить gitea-репо + префикс, роутить Plane-синк в свой проект задачи.
## Альтернативы
- Один репо на всё — отклонён (источник инцидента).
- Хардкод маппинга в коде — отклонён в пользу env-конфигурируемого реестра с безопасным дефолтом.
## Последствия
- Изоляция проектов на уровне вебхуков и роутинга.
- Парсер устойчив: битый элемент скипается, пустой результат → дефолт.
- Основа для `is_self_hosting_repo` (adr-0003).
## Связи
adr-0003 (условный гейт опирается на repo из реестра).

View File

@@ -1,23 +0,0 @@
# adr-0002: Очередь задач вместо in-process потоков
- **Статус:** accepted
- **Дата:** 2026-06-03
- **Задача:** ORCH-1 (F-2b)
## Контекст
Ранняя версия запускала стадии конвейера в in-process daemon-потоках. Проблемы: не переживало рестарт (задачи терялись), нет контроля параллелизма, нет ретраев, нет наблюдаемости.
## Решение
Введена персистентная очередь задач (`src/queue_worker.py` + таблица `jobs` в SQLite): atomic claim задачи воркером, `max_concurrency`, ретраи при сбое, restart-safe (running-задачи реквестятся при старте), эндпоинт `GET /queue`.
## Альтернативы
- In-process потоки — отклонены (не restart-safe).
- Внешний брокер (Redis/RabbitMQ) — избыточно для текущего масштаба; SQLite-очередь проще и без новых зависимостей.
## Последствия
- Конвейер переживает рестарт контейнера.
- Контроль параллелизма и наблюдаемость через `/queue`.
- ⚠️ Очередь общая на все проекты прод-инстанса — фактор группового риска при self-hosting (см. `docs/operations/INFRA.md`).
## Связи
adr-0001 (реестр проектов), INFRA.md (общая очередь при self-hosting).

View File

@@ -1,27 +0,0 @@
# adr-0003: Условный staging-гейт перед прод-деплоем
- **Статус:** accepted
- **Дата:** 2026-06-05
- **Задача:** ORCH-35
## Контекст
Оркестратор дорабатывает сам себя (self-hosting). Раньше стадия `deploy` имела «бумажный» вердикт: deployer-агент писал `deploy_status: SUCCESS`, но реального прогона на изолированной среде не было. Нужен предохранитель: прод-деплой орка не должен происходить, пока изменения не проверены на живой staging-среде. При этом другие проекты (enduro-trails) staging-инфры не имеют.
## Решение
Добавлена промежуточная стадия `deploy-staging` между `testing` и `deploy`: `testing → deploy-staging → deploy → done`.
- deployer гоняет `scripts/staging_check.py --base-url http://localhost:8501` и пишет `staging_status: SUCCESS|FAILED` в `15-staging-log.md`.
- Quality Gate `check_staging_status` парсит вердикт (только YAML-frontmatter).
- **Гейт условный:** `is_self_hosting_repo(repo)` → реальная проверка только для `orchestrator`; для остальных проектов гейт = no-op `(True, "Staging gate N/A")`.
- FAILED → откат на `development`.
## Альтернативы
- Глобальный гейт для всех проектов — отклонён: у enduro нет staging-инстанса, задачи застревали бы на откате.
- Деплой реально дёргает хост-хук прямо здесь — отложен в ORCH-36 (Вариант B).
## Последствия
- Прод-деплой орка недостижим, пока staging-гейт не зелёный.
- Другие проекты не затронуты (no-op).
- Реальный docker-деплой через хук пока НЕ выполняется (вердикт «бумажный», но подкреплён прогоном сьюта). Исполняемый деплой — ORCH-36.
## Связи
adr-0001 (реестр проектов — основа `is_self_hosting_repo`), ORCH-34 (deploy-hook + rollback), ORCH-36 (исполняемый самодеплой).

View File

@@ -1,45 +0,0 @@
# adr-0004: Поллинг с ретраем в quality-gate check_ci_green (фикс CI-race)
- **Статус:** accepted
- **Дата:** 2026-06-05
- **Задача:** ORCH-045
## Контекст
Quality-gate `check_ci_green(repo, branch)` (`src/qg/checks.py`) проверяет combined commit-status ветки через Gitea API сразу после того, как developer-агент запушил код. Реализация была **single-shot**: один `GET /repos/{owner}/{repo}/commits/{branch}/status`, чтение `data["state"]``success` → пропуск, иначе → сразу `False`.
Это создавало race condition. Gitea-CI после пуша 13 секунды держит combined state `pending`, пока не отработают чек-раннеры. Если гейт опрашивал статус в этом окне, он получал `pending` и возвращал `False` **ровно один раз** — повторного опроса не было. Combined state затем дозеленевал до `success`, но гейт уже промахнулся, и задача застревала насмерть без видимой причины.
Реальный инцидент **ORCH-017**: гейт опросил статус в 17:58:54 → `pending`; CI дозеленел в 17:58:55. Задача встала в тупик (см. `docs/history` / lessons ORCH-017).
## Решение
`check_ci_green` превращён из single-shot в **polling с ретраем**:
- `state == "success"``(True, "CI green")` немедленно.
- `state in ("failure", "error")``(False, "CI state: <state>")` немедленно — CI красный, ретрай бессмыслен (терминальное состояние).
- `state == "pending"` (или `unknown` / иное не-терминальное) → `time.sleep(interval)` и опрос снова, до `N` попыток.
- После исчерпания всех попыток при всё ещё `pending``(False, "CI still pending after <T>s")`**явный** провал с причиной, чтобы оператор видел тупик, а не молчаливый стол.
- `404``(False, "Branch ... not found or no status")` — как раньше.
- Транзиентная `httpx.HTTPError` на отдельной попытке — **не падаем сразу**: логируем и пробуем ещё в рамках лимита попыток; если все попытки — сетевая ошибка → `(False, "API error: <e>")`.
Параметры вынесены в `src/config.py` (pydantic-settings, env-prefix `ORCH_`, единый стиль с остальными настройками):
- `ci_poll_max_attempts` (env `ORCH_CI_POLL_MAX_ATTEMPTS`, дефолт **12**)
- `ci_poll_interval_s` (env `ORCH_CI_POLL_INTERVAL_S`, дефолт **10**)
Итого по умолчанию гейт ждёт `pending` до ~2 минут (12 × 10s) перед тем как явно провалиться. Каждая не-финальная попытка логируется через существующий `logger` (`check_ci_green: attempt i/N, state=..., retrying in Ns`). `timeout=10` на каждый отдельный запрос сохранён.
Сигнатура `check_ci_green(repo, branch) -> tuple[bool, str]` **не менялась** — её зовёт stage_engine и реестр гейтов `QG_CHECKS`.
## Альтернативы
- **Оставить single-shot, опрашивать гейт повторно снаружи (на уровне stage_engine/воркера).** Отклонено: размазывает логику CI-ожидания по слоям, дублирует таймауты; гейт — естественное место знания о combined-status.
- **Webhook от Gitea на завершение CI вместо поллинга.** Отложено: требует надёжной доставки/дедупликации вебхуков именно по CI-статусу и переписывания триггера стадии; поллинг — минимальный, локализованный фикс race-а здесь и сейчас.
- **Бесконечный ретрай до зелёного.** Отклонено: задача могла бы висеть вечно при реально зависшем CI; ограниченный бюджет + явный `False` с причиной даёт оператору сигнал.
## Последствия
- CI-race ORCH-017 закрыт: транзиентный `pending` переживается ретраем, гейт не промахивается.
- `check_ci_green` теперь **блокирующий** до ~`max_attempts × interval` секунд при затяжном `pending` (по умолчанию ~2 мин). Это осознанный trade-off; для красного CI и success — выход немедленный, без задержки.
- Тупик больше не молчаливый: истечение попыток → `(False, "CI still pending after <T>s")`, причина видна.
- Бюджет/интервал настраиваемы через env без правки кода.
- `check_tests_passed` / `_parse_tests_verdict` (ORCH-47) **не затронуты**.
## Связи
ORCH-017 (инцидент-первоисточник: deadlock shared-gate из-за CI-race), реестр гейтов `QG_CHECKS` (`check_ci_green`), стадия `development`. Тесты: `tests/test_qg.py::TestCheckCIGreen`.

View File

@@ -1,42 +0,0 @@
# adr-0005: Контейнеры оркестратора бегут под uid:gid хоста (1000:1000)
- **Статус:** accepted
- **Дата:** 2026-06-06
- **Задача:** ORCH-040
## Контекст
Оба контейнера (`orchestrator`, `orchestrator-staging`) запускались под `uid=0 (root)` и
монтировали хостовый `/home/slin/repos``/repos` (rw). Claude-CLI агенты исполняются
`subprocess.Popen` внутри контейнера под тем же root, поэтому все артефакты конвейера
(git worktree, коммиты в `docs/`) появлялись на хосте как `root:root`. Деплой прода под
`slin` (uid 1000) ломался на правах git до ручного `chown`. Это сквозное свойство рантайма:
касается агентов **всех** проектов, а не отдельной фичи.
## Решение
Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"` (uid:gid хоста `slin`).
- `group_add: ["999"]` сохраняется — доступ к docker.sock идёт через gid 999, не через root.
- target SSH-маунта приведён к `/home/slin/.ssh` (был `/root/.ssh`), синхронно с
`HOME=/home/slin`, который форсит launcher → единый HOME по осям uid/claude/ssh.
- Образ и launcher не меняются: numeric uid не требует записи в `/etc/passwd`,
`git config --system safe.directory '*'` уже есть.
Обязательные host-prerequisites (Owner, вне кода): доступ uid 1000 к
`/home/slin/.claude/.credentials.json` (блокер), ssh-ключи в новом HOME, рестарт prod
только в окно тишины. Детали и команды — work-item ADR-001 и `docs/operations/INFRA.md`.
## Альтернативы
- **drop-privileges только для subprocess агента** (`gosu`/`setuid`) — контейнер остаётся
root; новый код в горячем пути launcher, два uid в одном контейнере; отклонён.
- **chown-хук после каждой стадии** — лечит симптом, требует root внутри контейнера
(несовместимо), хрупкий пост-шаг; отклонён (fallback на крайний случай).
## Последствия
- Артефакты создаются под `slin:slin`; деплой прода не требует ручного `chown`.
- HOME консистентен (uid = claude = ssh = `/home/slin`); устранён рассинхрон SSH-маунта.
- Появляется явная привязка рантайма к uid 1000 хоста (задокументирована в INFRA.md).
- Прод-рестарт self = групповой риск (общий инстанс с enduro-trails) → строго окно тишины;
страховка — staging-гейт (adr-0003).
## Связи
adr-0003 (staging-гейт — обязательная проверка перед прод-рестартом self),
adr-0001 (`is_self_hosting_repo`), work-item `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md`.

View File

@@ -1,53 +0,0 @@
# adr-0006: Merge-gate — догон `main` + re-test + сериализация слияний
- **Статус:** proposed
- **Дата:** 2026-06-06
- **Задача:** ORCH-043
- **Детальный ADR:** `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`
## Контекст
Ветка валидируется относительно того `main`, из которого создана, а не относительно `main`
на момент слияния. Параллельная задача могла влиться раньше → **семантический конфликт
слияния** (git мержит без текстового конфликта, но `main` сломан). Для self-hosting это
красный `main` инструмента, обслуживающего все проекты. Слияние в `main` делает
deployer-агент в начале стадии `deploy`; замена механизма PR-merge — вне объёма.
## Решение
Детерминированный merge-gate (`check_branch_mergeable`, без LLM) на ребре
`deploy-staging → deploy`, ДО запуска deployer'а, который мержит. `STAGE_TRANSITIONS` не
меняется (минимальный blast-radius); в `QG_CHECKS` добавлен `check_branch_mergeable`.
- **Догон:** ветка отстаёт ⇔ `origin/main` не предок HEAD → `rebase origin/main` в worktree
+ `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт →
`rebase --abort` → откат на `development`.
- **Re-test:** `python -m pytest tests/` в worktree догнанной ветки, тайм-аут
`merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
- **Сериализация (BR-5):** файловый **merge-lease** на репо
(`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge.
Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer**
(re-enqueue deployer с задержкой через `available_at`), не rollback. Release — на
PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe.
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги
`merge_gate_enabled` / `merge_gate_repos` для поэтапного раската.
## Альтернативы
- **Новая стадия `merge-gate`** (кандидат B) — «пустая» стадия без агента не имеет триггера
(`advance_stage` срабатывает только на завершении агента/вебхуке); потребовала бы chaining
в движке (не restart-safe) или синтетический job-тип. Отклонено.
- **Перенос merge в детерминированный шаг оркестратора** (кандидат C) — запрещён объёмом
(замена механизма PR-merge вне scope). Отклонено.
- **Блокирующий lock** — дедлок при одном worker-слоте. Отклонено в пользу defer.
## Последствия
- Сценарий «две зелёные ветки ломают `main`» закрыт: re-test против актуального `main` +
сериализация слияний.
- Плата: merge-gate — «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); сериализация
опирается на PR-merged вебхук со страховкой реклеймом по возрасту; defer перепрогоняет
staging; длинный re-test держит worker-слот.
- Сквозное изменение конвейера → `arch:major-change`; прод-деплой ORCH-043 строго через
staging-гейт (8501).
## Связи
adr-0001 (`is_self_hosting_repo`), adr-0003 (условный staging-гейт — образец условности),
adr-0002 (очередь / `available_at` для defer), ORCH-2 (worktree-изоляция), ORCH-046
(дословный reason в `task_desc` при откате).

View File

@@ -1,64 +0,0 @@
# ADR-0007: Исполняемый самодеплой стадии `deploy` (Вариант B, ORCH-36)
## Статус
Accepted (design) — реализация в ветке `feature/ORCH-036`.
## Контекст
Стадия `deploy` была «бумажной»: deployer-агент писал `deploy_status:` в
`14-deploy-log.md`, гейт `check_deploy_status` парсил вердикт и двигал
`deploy → done`. Реального деплоя не было. ORCH-36 делает стадию исполняемой для
self-hosting (`orchestrator`), сохраняя прежний ssh-путь для остальных репо.
Три ограничения формируют дизайн (детально — `docs/work-items/ORCH-036/06-adr/ADR-001`):
1. **Self-restart**: рестарт прод-контейнера 8500 убивает in-container процесс →
рестарт делает ВНЕШНИЙ host-процесс.
2. **Status-only verdict model**: approve = смена статуса Plane на `Approved`
(комментарии не управляют конвейером).
3. **Гонка гейта**: вердикт нельзя читать до завершения асинхронного хука.
## Решение
Для self-hosting стадия `deploy` исполняется в три фазы детерминированным кодом
(без LLM в критическом пути self-restart):
- **Фаза A (вход в `deploy`)** — для self + `deploy_require_manual_approve=true`
вместо запуска прод-deployer выставляется approval-pending статус Plane + запрос
approve (Plane-коммент + Telegram). Перехват в `advance_stage` на ребре
`deploy-staging → deploy` (после `check_staging_status` и merge-gate).
- **Фаза B (Plane → Approved)** — `advance_stage(deploy, finished_agent=None)`
запускает **detached host-процесс** (ssh + setsid → `orchestrator-deploy-hook.sh`
с прод-параметрами и build-once retag) и ставит **детерминированный finalizer-job**
с задержкой; маркер `initiated` — идемпотентность. Возврат БЕЗ advance.
- **Фаза C (finalizer)** — после рестарта новый контейнер дочитывает sentinel
`result` (exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет
`14-deploy-log.md`, вызывает `advance_stage(deploy, finished_agent="deployer")`
→ существующие контракты: `SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
### Ключевые инварианты (НЕ меняются)
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status` / `_parse_deploy_status`
(frontmatter only), откат БАГ-8, terminal-sync `deploy → done`, merge-gate (ORCH-43),
exit-code-контракт хука (0/1/2).
### Новое (сквозное)
- **Детерминированный job-kind** `deploy-finalizer` в очереди (reserved-agent, не
LLM): read-result | defer | map+write+advance. Зеркалит детерминизм merge-gate.
- **Approve-флаг** `deploy_require_manual_approve` (дефолт `true`; полный авто —
отдельная задача после набора метрик доверия, ORCH-54).
- **Build-once**: опциональный `SOURCE_IMAGE` retag в хуке (обратно совместимо).
- **Restart-safe состояние** деплоя — sentinel-файлы под
`<repos_dir>/.deploy-state-<repo>/<wi>/` (как merge-lease), БЕЗ миграции БД.
### Условность
Вся логика — только для `is_self_hosting_repo(repo)` (как ORCH-35). Прочие репо
деплоятся прежним синхронным ssh-путём агентом.
## Последствия
- `deploy_status: SUCCESS` доказан реальным health-ok; критический путь self-restart
детерминирован.
- Вводится новая под-компонента (finalizer job-handler) → изменение помечено
`arch:major-change`.
- Approve вписан в status-only модель: restart-safe, аудируемо, идемпотентно.
- На старте — обязательный ручной approve; молчаливых деплоев нет (Plane+Telegram).
## Связанные ADR
`adr-0003` (staging-gate), `adr-0006` (merge-gate), `adr-0005` (run-as-host-uid).
Детальный per-work-item: `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.

View File

@@ -1,86 +0,0 @@
# adr-0007: Reconciler застрявших стадий (sweeper потерянных webhook)
- **Статус:** accepted (реализовано в `src/reconciler.py`)
- **Дата:** 2026-06-06
- **Задача:** ORCH-053
- **Детальный ADR:** `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`
## Контекст
Конвейер продвигается **только** входящими webhook (Plane status / Gitea CI/PR).
Потерянное событие (502 на ребилде, отсутствие ретраев у Plane/Gitea,
неразрезолвленный `sha→branch`) → источник истины изменился, а стадия задачи —
нет; задача застревает молча (инцидент ORCH-044). Существующий resilience
(`requeue_running_jobs`, orphan-recovery, events de-dup ORCH-5, `ci_poll`
ORCH-045) работает на уровне jobs/agent_runs и **не реконсилирует**
рассинхрон «источник истины ≠ стадия задачи».
## Решение
Фоновый daemon-поток `src/reconciler.py` (паттерн `queue_worker`, module-singleton,
`threading.Event`), стартует в `main.lifespan` после `worker.start()`, стоп в
`finally` перед `worker.stop()`. Две взаимодополняющие ветки на каждом тике
(`reconcile_interval_s`, дефолт 120с):
- **F-1 gate-side** (локальная БД): для каждой `task` где `stage∉{done}`, **нет**
активного job, `age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка
канонического QG стадии; если зелёный → продвижение **штатным**
`stage_engine.advance_stage(..., finished_agent=None)` (тот же путь, что у Plane
Approved-webhook). Красный → **тишина** (нет advance, нет нотификаций — спам
структурно невозможен). `analysis` F-1 **не** реконсилирует (человеческий гейт →
отдан F-2).
- **F-2 plane-side** (опрос Plane API per-project через `list_issues_by_state`):
`In Progress`+нет задачи → `handle_status_start`; `Approved`+не сдвинута →
`handle_verdict(approved=True)`; `Rejected`+не откатана →
`handle_verdict(approved=False)`. Обработчики `webhooks/plane.py`
**переиспользуются** (async → `asyncio.run` из sync-потока), логика не дублируется.
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по
`repo`+`stage='development'`, видимость на INFO) — defense-in-depth.
**Инварианты:** источник истины — гейт/Plane, не событие; продвижение только через
`advance_stage`; идемпотентность (active-job guard + atomic-claim на создании +
grace + `max_concurrency=1`); never-raise на единицу работы; тишина при
синхронности; restart-safe; kill-switch.
## Альтернативы
- **Флаг подавления нотификаций в `advance_stage`** — отклонён: меняет общий
критический путь. Вместо этого «не вызывать advance_stage на красном гейте».
- **UNIQUE-индекс `tasks.plane_id`** для анти-дубля — отклонён как primary: риск
падения миграции на проде; выбран process-wide `threading.Lock` (single-process
топология). Индекс — задокументированное будущее упрочнение для multi-process.
- **Отдельная стадия/QG реконсиляции** — вне объёма; нарушает «источник истины —
существующий гейт».
- **Реконсиляция analysis по локальным артефактам** — отклонена: автопродвижение
неодобренного человеком BRD.
## Последствия
- Потерянный webhook ≠ молча застрявшая задача; ручной heartbeat-watchdog не нужен;
резервная сетка к ORCH-51 (буфер недоставленных) и ORCH-36 (deploy).
- Плата: фоновый поток + опрос Plane API (митигируется интервалом/фильтром/
per-project); двойная оценка гейта на зелёной задаче; анти-дубль опирается на
single-process-допущение (как и очередь ORCH-1).
- Self-hosting: `reconcile_enabled` — обязательный kill-switch; поэтапный раскат
(`reconcile_plane_enabled` гасит только F-2); reconciler не рестартит/не роняет
прод-контейнер. БД-схема и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются.
## Уточнения
- **ORCH-060** (`docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md`):
F-1 (`_reconcile_gate_task`) приобретает два пред-гарда ДО оценки гейта —
пропускает escalated (`developer_retry_count ≥ MAX_DEVELOPER_RETRIES`,
детерминированно) и Blocked/Needs-Input (Вариант A, Plane API, без миграции)
задачи. Инварианты 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 как под-гейт ребра
внутри `advance_stage`), adr-0001 (реестр проектов для F-2 per-project), ORCH-5
(events de-dup — защита от дублей; reconciler — обратная защита от потерь),
ORCH-045 (`ci_poll`).

View File

@@ -1,77 +0,0 @@
# ADR-0008: Провенанс staging-образа перед BUILD-ONCE retag в прод (ORCH-058)
## Статус
Accepted (design) — реализация в ветке `feature/ORCH-058-self-deploy-retag-staging`.
Метка: `arch:major-change`.
> Примечание о нумерации: в `adr/` исторически два файла `adr-0007-*`
> (`executable-self-deploy`, `reconciler`) — пред-существующая коллизия. Этот ADR берёт
> следующий свободный номер **0008**; коллизию 0007 не трогаем (вне объёма ORCH-058).
## Контекст
ORCH-36 (`adr-0007-executable-self-deploy`) сделал стадию `deploy` исполняемой для
self-hosting: Phase B запускает host-хук, который шагом **2b** (BUILD-ONCE) делает
`docker tag $SOURCE_IMAGE → $TARGET_IMAGE` **без rebuild** — «прод = ровно тот артефакт,
что прошёл staging». Предпосылка: staging-образ свеж и собран из провалидированного кода.
**Этой гарантии нет.** Конвейер нигде не пересобирает `orchestrator-orchestrator-staging`
из провалидированного коммита; `deploy-staging` лишь гоняет `staging_check.py` против уже
работающего 8501. Инцидент (LESSONS_ORCH-036 п.4): staging-образ не пересобрали → проверка
прошла против старого кода → retag промоутнул СТАРЫЙ образ → прод **молча** откатился на
2-дневный код. Зелёный гейт = ложный позитив. Самый опасный из 4 багов: не падает, а тихо
откатывает инструмент, обслуживающий все проекты.
## Решение
Гарантировать `INV-FRESH`: в прод промоутится только образ, собранный из коммита,
провалидированного `deploy-staging` для данной задачи; иначе fail-fast (`FAILED` → откат на
`development`, БАГ-8), прод не трогается. Достигается **двумя взаимодополняющими слоями**
(defense in depth), только для self-hosting (условность как ORCH-35/36/43):
- **A — пересборка (liveness).** На ребре `deploy-staging → deploy`, ПОСЛЕ merge-gate и ДО
Phase A, детерминированный QG-под-чек `check_staging_image_fresh` пересобирает
`orchestrator-orchestrator-staging` из worktree валидированного коммита
(`--build-arg GIT_SHA=<sha>`, лейбл `org.opencontainers.image.revision`), пересоздаёт 8501
и прогоняет `staging_check`. FAIL → откат на `development`. Так валидируемый и промоутимый
артефакт — один и тот же; гарантирует наличие зелёного пути (нет вечного fail-fast).
- **B — fail-closed guard (safety).** Хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл
`revision` образа `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`).
Несовпадение / пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED.
Делает тихий промоут устаревшего образа структурно невозможным даже при отключённой/
проигравшей гонку A.
**Якорь провалидированного коммита**`git rev-parse HEAD` в worktree ПОСЛЕ merge-gate
(post-rebase tree, который ре-тестирован и сольётся в `main`). Один helper
`validated_revision(repo, branch)` питает и штамп сборки (A), и `EXPECTED_REVISION` (B).
**Условность и kill-switch:** единый `image_freshness_enabled` (вкл/выкл A+B как целое,
чтобы не было «B без A» = вечный fail-fast), `image_freshness_repos` (CSV; пусто →
self-hosting). Все настройки с префиксом `ORCH_`.
### Что НЕ меняется
`STAGE_TRANSITIONS` (набор стадий — под-гейт ребра, не стадия), exit-code хука (0/1/2),
`map_exit_code_to_status`, `check_deploy_status`/`_parse_deploy_status`, БАГ-8, terminal-sync,
merge-gate, Phase A/B/C. Схема БД — без миграций (провенанс в лейбле образа, не в БД).
### Что добавляется (сквозное)
- QG `check_staging_image_fresh` в реестре `QG_CHECKS` (+ snapshot-тест), wired через
`_handle_image_freshness` в `stage_engine` (рядом с merge-gate).
- Режим хука `--build-staging` (build из worktree + recreate 8501; STAGING-safe дефолты).
- OCI-лейбл `org.opencontainers.image.revision` в `Dockerfile` (`ARG GIT_SHA`).
- Helpers `validated_revision` / `rebuild_staging_image` в `self_deploy.py` (never-raise).
## Последствия
- Класс «тихого регресса прод» закрыт структурно (B); валидный деплой всегда доходит до
зелёного (A) — устранён ручной bootstrap-разрыв пересборки staging.
- Латентность ребра растёт (build + recreate + повторный staging_check); `staging_check`
гоняется дважды (soft pre-check агента + авторитетный код) — плата за «валидируем =
промоутим».
- Все сборки/recreate — ТОЛЬКО staging (8501); прод (8500) не трогается; `main` не пушится.
Новая под-компонента → `arch:major-change`.
## Связанные ADR
`adr-0007-executable-self-deploy` (BUILD-ONCE, Phase A/B/C), `adr-0006-merge-gate` (образец
edge-под-гейта), `adr-0003-staging-gate` (условность self-hosting), `adr-0005`
(run-as-host-uid). Детальный per-work-item: `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.

View File

@@ -1,56 +0,0 @@
# adr-0009: Толерантность staging-вердикта к заведомо инфраструктурным FAIL
- **Статус:** accepted
- **Дата:** 2026-06-07
- **Задача:** ORCH-061
- **Детально:** `docs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md`
## Контекст
Self-hosting `orchestrator` зацикливался на `deploy-staging`: `staging_check.py`
давал 2 ложных FAIL (C9a — ветка в sandbox, C9b — analyst-job в очереди), вызванных
отсутствием sandbox-настроек (bot-аккаунты не члены SANDBOX-проекта), а не регрессом
кода. `staging_check.py` делал `sys.exit(1)` при любом FAIL → deployer писал
`staging_status: FAILED``check_staging_status` FAILED → откат `deploy-staging →
development` → петля (жгла developer-ретраи и кредиты). Прод-деплой орка приходилось
доводить вручную — блокер автономного внедрения (ORCH-54).
## Решение
Классифицировать проверки staging-suite на **REAL** (pipeline) и **SANDBOX_INFRA**
(заведомо инфраструктурные, узкий allowlist `{C9a, C9b}`) и сделать вердикт
толерантным к инфра-FAIL, сохранив fail-closed для реальных проверок:
- Новый leaf-модуль `src/staging_verdict.py` (pure, never-raise, stdlib):
`classify_check(label)` + `compute_staging_verdict(items, infra_tolerant)`.
Правило: упала хоть одна REAL → FAILED/exit1; упали ТОЛЬКО SANDBOX_INFRA и
толерантность вкл → SUCCESS/exit0 (waived); толерантность выкл → legacy strict
(любой FAIL → FAILED).
- `scripts/staging_check.py` помечает проверки категориями, считает вердикт через
`staging_verdict`, печатает `INFRA-WAIVED` при вайвере (наблюдаемость).
- Kill-switch `staging_infra_tolerance_enabled` (env
`ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `True`; в `.env.staging`).
- `check_staging_status` / `_parse_staging_status` / `STAGE_TRANSITIONS` / реестр
`QG_CHECKS`**без изменений**; новый QG-чек не вводится. Условность ORCH-35
сохранена (не-self → no-op N/A).
- Инвариант FR-3: «no changes to commit» на action-стадиях (`deploy-staging`/`deploy`)
не есть недовыполнение — продвижение определяется exit0 + гейт-вердиктом
(launcher уже не откатывает; добавлена observability-строка).
## Альтернативы
- Только починить sandbox-инфру (направление а) — хрупко, не структурно, вне
автономной досягаемости таска; оставлено как опциональное hardening.
- «Зелёный по умолчанию» при недоступности проверок — запрещён (fail-closed).
- Новый QG-чек / структурный артефакт `15-staging-log.md` — избыточно, меняло бы
контракты/реестр; толерантность размещена в suite до артефакта.
## Последствия
- Петля устранена; страховка цела (реальный регресс → FAILED → откат).
- Чистая вердикт-логика юнит-тестируема без live staging/docker.
- Контракты гейтов/стадий/вердиктов/реестра и схема БД неизменны.
- Риск: узкое окно — реальный регресс именно в создании ветки/постановке
analyst-job может быть заваивен; митигировано allowlist'ом `{C9a,C9b}` + условием
«все REAL (вкл. C7/C8) зелёные» + INFRA-WAIVED-логом. Разблокирует ORCH-54.
## Связи
adr-0003 (условный staging-гейт — база `is_self_hosting_repo` / `check_staging_status`),
adr-0006 (merge-gate), adr-0007 (исполняемый self-deploy), adr-0008 (провенанс
staging-образа). Блокирует ORCH-54.

View File

@@ -1,85 +0,0 @@
# adr-0010: Post-deploy мониторинг прода + реакция на деградацию
- **Статус:** proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`
- **Дата:** 2026-06-07
- **Задача:** ORCH-021
- **Метка:** `arch:major-change` (новая под-компонента + новый reserved-agent job-kind)
- **Детальный ADR:** `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`
## Контекст
Конвейер заканчивается на `deploy → done`: `check_deploy_status` видит
`deploy_status: SUCCESS` → terminal-sync (Plane → Done, release merge-lease), и
оркестратор **забывает про прод**. «Успех» сегодня = health-check в момент рестарта
(~60с окно в `orchestrator-deploy-hook.sh`). Класс инцидентов «зелёный деплой, красный
прод» (прецедент **ET-8**): деградация проявляется через минуты под боевым трафиком,
health отвечает `200 ok`, фича сломана. Для self-hosting опасно вдвойне — сломанный
прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса.
## Решение
Продлить ответственность конвейера **ЗА** `done`: после терминального перехода для
применимого репо армится пост-деплой наблюдение окна `post_deploy_window_s` (дефолт
~15 мин) с интервалом `post_deploy_interval_s`; деградация фиксируется по
**детерминированным порогам**, при подтверждении выполняется реакция.
**Механизм — reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`,
ORCH-36), НЕ отдельная стадия и НЕ daemon-поток:
- **Арм:** в `stage_engine.advance_stage`, в блоке `next_stage == "done"`, при
`post_deploy.post_deploy_applies(repo)``post_deploy.arm_monitor(...)` (sentinel
`armed` = идемпотентность, первый job через `enqueue_job(available_at_delay_s=...)`).
- **Тик:** `launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО
`_spawn``stage_engine.run_post_deploy_monitor(job)`: один опрос сигналов, append в
персистентный `series`, классификация; HEALTHY и окно не истекло → перепостановка с
задержкой; иначе → реакция + артефакт + `mark_done`.
- **Чистая логика — новый leaf-модуль `src/post_deploy.py`** (never-raise, по образцу
`self_deploy.py`/`staging_verdict.py`): `post_deploy_applies`, `probe_signals`
(опрос `/health` + доля 5xx на `/status`,`/queue`), `classify` (HEALTHY|DEGRADED —
главный предмет юнит-тестов), `decide_action` (NONE|ROLLBACK|ALERT_ONLY с учётом
self-hosting), sentinel-state хелперы, `write_post_deploy_log`.
**Сигналы и пороги (детерминированно, AC-3…AC-6):** `DEGRADED``≥
post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx на окне `>
post_deploy_5xx_threshold`. Одиночный глюк < порога → HEALTHY (нет ложных откатов).
**Реакция (BR-4/BR-5):**
- **Self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY`:** громкий Telegram + Plane,
запрос ручного approve отката. Тик НИКОГДА не откатывает/рестартит прод-контейнер
(структурный инвариант). Откат прод-орка, если оператор решит, — только detached
host-процесс (`self_deploy.initiate_deploy`), вне тика (MVP).
- **Не-self + `post_deploy_auto_rollback=True`:** хук `--rollback` с прод-env; exit
`0 → ROLLBACK_OK`, `1/2 → ROLLBACK_FAILED` + громкий алерт.
- Дефолт (`auto_rollback=False`) → `ALERT_ONLY`.
**Артефакт `16-post-deploy-log.md`** (новый) с YAML-frontmatter (`post_deploy_status`,
`action_taken`, `window_s`, `checks_total/failed`) — машиночитаемо для петли уроков
ORCH-8; best-effort. **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец
`reconcile.status()`).
## Альтернативы
- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия опросов в памяти не
restart-safe (а деплой орка = рестарт); restart-safe-вариант требует тех же sentinel,
reserved-agent проще и уже имеет проверенную jobs+sentinel машинерию.
- **Отдельная пост-deploy стадия + QG** — отклонён: меняет `STAGE_TRANSITIONS`/
`QG_CHECKS`, ломает семантику терминального `done`; наблюдение принципиально ПОСЛЕ
`done`.
- **Авто-rollback прод-орка из тика** — отклонён (self-hosting safety): групповой риск;
контейнер не откатит себя надёжно. Self → alert + ручной approve (как ORCH-54).
- **Колонка в `tasks`** — отклонён: миграция на проде; sentinel-файлы restart-safe
(как ORCH-36/53/58).
## Последствия
- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация =
сигнал для ORCH-8.
- Реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`), контракт `check_deploy_status`,
terminal-sync, merge-gate, exit-code-контракт хука, схема БД — **не меняются**.
- Дефолты безопасны: kill-switch on, auto-rollback off, self только alert.
- Ограничение: монитор self бежит внутри наблюдаемого прода — полностью wedged
контейнер = пропущенный тик/алерт (known MVP gap; внешний watchdog — follow-up).
- Self-hosting: тик не рестартит/не роняет прод-контейнер; kill-switch
`post_deploy_monitor_enabled` обязателен; поэтапный раскат через `post_deploy_repos`.
## Связи
adr-0007-executable-self-deploy (ORCH-36 — sentinel/detached-host/finalizer образец,
`map_exit_code_to_status`), adr-0007-reconciler (ORCH-53 — daemon/`status()` образец,
отклонён как основной механизм), adr-0006 (merge-gate — условность/флаги раската),
adr-0003 (staging-gate — образец условности), adr-0008 (provenance — `.deploy-prev-image`/
хук-откат). Прецедент ET-8. Будущее: ORCH-8 (петля уроков), ORCH-54 (полный авто).

View File

@@ -1,82 +0,0 @@
# adr-0011: Job-reaper + проактивный реклейм merge-lease
| | |
|---|---|
| Статус | accepted |
| Дата | 2026-06-07 |
| Источник | ORCH-065 (BUG P0, блокер ORCH-54) |
| Детально | `docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md` |
## Контекст
Единый инстанс с общей БД и очередью (`jobs`, `max_concurrency=1` для
self-hosting). Финализация статуса job (`done`/`queued`/`failed`) происходит
ТОЛЬКО в `launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
self-restart во время deploy) оставляет строку `jobs` навсегда `running`. При
`max_concurrency=1` одна такая зомби-строка блокирует claim всех job →
**встаёт конвейер всех проектов**. Единственная защита — `requeue_running_jobs()`
— работает ТОЛЬКО на старте процесса. Симметрично: merge-lease (ORCH-043,
файл `.merge-lease-<repo>.json`) реклеймится лишь лениво по TTL при чужом
`acquire`; liveness держателя по pid не проверяется → залипший lease блокирует
чужие merge. Это последняя ручная точка автономного self-deploy (блокер ORCH-54);
доказанные инциденты 07.06 — jobs 236/239/242/254.
## Решение
1. **Job-reaper** — новый daemon-поток `src/job_reaper.py` (каркас `reconciler`:
never-raise, `_stop`-Event, старт/стоп в `lifespan`, снимок в `/queue`,
kill-switch). Работает **без рестарта** процесса. Liveness — трёхуровневая:
Tier-1 мёртвый `jobs.pid` (новая колонка) после `reaper_dead_ticks` подряд
тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running` — но только
после finalization-grace `reaper_finalize_grace_s` (окно неоднозначно: живой
monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии, поэтому
живой финализирующий monitor НЕ реапится); Tier-3 backstop по потолку
`reaper_max_running_s`. Действие — **claim-before-act**: для exit0 канонический
QG оценивается read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и
только победитель claim выполняет `_try_advance_stage` (advance+enqueue) —
проигравший не делает побочных эффектов (источник истины — QG, не «exit0»);
гейт красный или exit≠0 / неизвестно → `attempts<max``queued`, иначе
`failed`+Telegram. Атомарный reap-claim (`UPDATE ... WHERE id=? AND
status='running'` + `rowcount`, как `claim_next_job`) исключает двойную
обработку (совместимость со стартовым `requeue_running_jobs`).
2. **Проактивный реклейм stale/dead lease** — функции в `merge_gate.py`
(`pid_alive`, `reclaim_stale_lease`), вызываемые на старте (рядом с
`requeue_running_jobs`) и периодически из тика reaper. Освобождение, если
держатель **мёртв** (pid не жив) ИЛИ **просрочен** (TTL); живой держатель в
пределах TTL — НЕ трогать. holder-aware, never-raise, условность как ORCH-43.
3. **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не
повторяются (`branch_is_behind_main==False`); добавлен детерминированный
never-raise guard `pr_already_merged` (читает состояние PR), консультируемый
перед повторным merge → уже слит = no-op.
4. **Схема БД**`jobs.pid INTEGER` через идемпотентный `_ensure_column`
(паттерн live-safe миграции). Больше ничего не меняется.
Kill-switch'и (`ORCH_*`): `reaper_enabled`, `reaper_interval_s`,
`reaper_dead_ticks`, `reaper_max_running_s`, `reaper_finalize_grace_s`,
`lease_reclaim_enabled`; переиспользуются `merge_lock_timeout_s`,
`merge_gate_repos`. `false` → строго прежнее поведение.
## Альтернативы
- Reaper внутри reconciler — отвергнуто (смешение stage- и jobs-уровней, общий
kill-switch, хуже изоляция).
- Только эвристика `agent_runs` без `jobs.pid` — отвергнуто как основной механизм
(не ловит зомби, чей monitor умер до записи exit_code); оставлена как Tier-2/3.
- БД-lock / внешний брокер очередей — вне объёма (single-node SQLite).
- Форс `done` по факту exit0 — отвергнуто; выбран gate-driven advance.
## Последствия
- (+) Зомби-job и залипший lease самовосстанавливаются без рестарта и без
оператора; очередь общего инстанса не встаёт; снят технический блокер ORCH-54.
- (+) Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
exit-коды хука); одна колонка через проверенный idempotent-паттерн.
- () pid-liveness валиден в предположении одного pid-namespace (агент —
дочерний процесс оркестратора); закрыто backstop'ом по времени и TTL.
- () streak-счётчик in-memory (сброс на рестарте; рестарт покрыт
`requeue_running_jobs`).
## Связи
- Базируется: adr-0002 (очередь), adr-0006 (merge-gate), adr-0007 (reconciler /
self-deploy).
- Разблокирует: ORCH-54.

View File

@@ -1,63 +0,0 @@
# adr-0012: Security-гейт — secret-scanning + dependency audit перед мержем
- **Статус:** proposed
- **Дата:** 2026-06-07
- **Задача:** ORCH-022
- **Детальный ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`
## Контекст
Оркестратор автономен: `developer` пишет код без человека-фильтра. Перед слиянием ветки в
`main` нет проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую
зависимость (CVE). Для self-hosting один общий прод-инстанс обслуживает все проекты с общей
БД — секрет/CVE через одну задачу попадает в прод всех (CLAUDE.md §self-hosting, §8). Фактический
мерж PR в `main` делает `deployer` в начале стадии `deploy`.
## Решение
Детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**,
рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди
edge-под-гейтов (ДО merge-gate). `STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавлен
`check_security_gate`. Паттерн — как у соседей: leaf-модуль `src/security_gate.py`
(never-raise) + тонкая обёртка в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
аллоулиста (`.gitleaks.toml`) → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
не зависит от сети.
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
громкий warning** (анти-петля; флаг `security_dep_audit_fail_closed` для строгого режима).
- **ПЕРВЫМ на ребре, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки
ДО rebase не «обвиняет» задачу в CVE, притащенной обновившимся `main` (анти-петля
ORCH-061); до захвата merge-lease → при FAIL lease освобождать не нужно.
- **Артефакт `17-security-report.md`** с YAML-frontmatter (`security_status`,
`secrets_found`, `deps_blocking`, `deps_warning`, `deps_audit_degraded`); вердикт читается
ТОЛЬКО из frontmatter (канон), negative-токен авторитетен; битый/нет → fail-closed.
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
- **Условность (как ORCH-35/43/58):** `security_gate_enabled` + `security_gate_repos`; пусто
→ реально только self-hosting (`orchestrator`), прочие репо — no-op pass.
- **never-raise**, таймаут `security_scan_timeout_s`, гейт не деплоит/не рестартит прод.
## Альтернативы
- **Вариант R (review-стадия):** diff может разойтись с мержем в `main`; merge-edge — последняя
страховка. Отклонено.
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
выражаются статусом коммита; коуплинг с раннером. Отклонено для v1 (точка расширения).
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как в ORCH-043).
Отклонено.
- **fail-closed dep-audit / аудит после rebase:** ложные откаты → петля. Отклонено.
- **Новая колонка retry в БД:** не нужна (переиспользуем `_developer_retry_count`).
## Последствия
- Класс «тихо влитый секрет/CVE» закрыт: секреты — безусловно (offline), CVE — best-effort при
доступности фида. Самоприменение CLAUDE.md §8 без человека.
- Плата: ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); внешние инструменты
(gitleaks в образе, pip-audit в зависимостях); время скана на каждом прогоне (ограничено
таймаутом); v1 — Python-only (SAST/мульти-стек — follow-up WI).
- Сквозное изменение (новый QG + edge-под-гейт) → `arch:major-change`; прод-деплой ORCH-022 —
строго через staging-гейт (8501), без рестарта прод-контейнера.
## Связи
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
условность/never-raise/fail-closed), adr-0003 (условный гейт / `is_self_hosting_repo`),
adr-0009 (анти-петля ложных FAIL, ORCH-061), ORCH-046 (дословный reason в `task_desc`),
ORCH-9/15 (мульти-стек — будущая зависимость), ORCH-2 (worktree-изоляция).

View File

@@ -1,63 +0,0 @@
# 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

@@ -1,77 +0,0 @@
# 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

@@ -1,47 +0,0 @@
# 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,84 +0,0 @@
# adr-0021: Канон Anthropic для системных промптов агентов + эмиссия frontmatter-схемы 52c
- **Статус:** proposed
- **Дата:** 2026-06-09
- **Источник:** ORCH-077 (эпик ORCH-52, слой 52d — замыкающий)
- **Связи:** реализует слой промптов к adr-0019 (52b, PIPELINE_DOCS) и adr-0020 (52c,
frontmatter-контракт). Детально — `docs/work-items/ORCH-077/06-adr/ADR-001-anthropic-prompt-canon.md`.
## Контекст
Эпик ORCH-52 строит сквозной контракт документации конвейера: **52b** (adr-0019) — описательный
стандарт документов + скелеты `docs/_templates/`; **52c** (adr-0020) — машинный контракт
`src/frontmatter.py` (reader/writer/валидатор `REQUIRED_FIELDS`) + спека `HANDOFF_PROTOCOL.md` с
обязательной 6-польной схемой `(work_item, stage, author_agent, status, created_at, model_used)`.
Две незакрытые проблемы:
1. **Цепочка 52b→52c→52d разорвана.** Writer и валидатор схемы есть, но работают warning-only
(`frontmatter_validation_strict=False`); агенты **не эмитят** поля схемы — на входе валидатора нет
данных, петля не замкнута.
2. **Форма 6 промптов `.openclaw/agents/*.md` разнородна** (RU/EN, свободная структура) → снижает
предсказуемость агентов прода, которые исполняются на КАЖДОЙ задаче ВСЕХ проектов из общего
инстанса (self-hosting).
Факт загрузки (сверено `src/agents/launcher.py`): промпт `cat`-ается из git-worktree агента в момент
запуска (`--system-prompt "$(cat .openclaw/agents/<role>.md)"`), НЕ запекается в образ.
## Решение
Ввести **обязательный канон формы** для всех агент-промптов и сделать его машинно-проверяемым.
1. **Фиксированный XML-скелет (5 обязательных секций, нормативный порядок):**
`<context>``<task>` (+ опц. `<thinking>`) → `<deliverables>``<constraints>`
`<output_format>`. Доп. секции (`<success_criteria>`, `<escalation>`) — после. Контекст/роль
вперёд, формат вывода последним (recency для следования схеме).
2. **Аддитивная эмиссия схемы 52c.** `<output_format>` каждого промпта перечисляет 6 полей схемы с
роле-специфичными значениями и инструктирует ставить их **рядом** с существующим machine-verdict
ключом, **не меняя его имя/регистр/значения** (`verdict:`, `result:`, `staging_status:`,
`deploy_status:`, `security_status:` — байт-в-байт). Для `04-test-plan.yaml` (чистый YAML) — как
top-level ключи. Гейты читают вердикты как раньше (схема в boolean-вердикте не участвует).
3. **Few-shot + позитивные альтернативы.** Ссылки на `docs/_templates/` и эталоны (ORCH-073/088);
каждый запрет в формате «❌ X → ✅ Y».
4. **CoT/thinking** у решающих ролей (architect/reviewer/tester/deployer).
5. **Анти-регресс машинно.** Структурные тесты `tests/test_agent_prompts_canon.py` (без запуска
агентов): 5 секций, 6 полей схемы, точный регистр machine-verdict ключей, ключевые
self-hosting-маркеры (deployer: `docker exec orchestrator-staging`, `pr_already_merged`,
«не рестартить 8500»). `test_agent_frontmatter_no_model.py` остаётся зелёным.
6. **Enforcement не включается.** `frontmatter_validation_strict` остаётся `False` (warning-only);
52d учит эмитить добровольно. Hard-fail — отдельная будущая задача.
**Границы:** docs/prompts-only. `src/**` (config, launcher, frontmatter, stages, qg/checks,
stage_engine), `STAGE_TRANSITIONS`, `QG_CHECKS`, состав machine-verdict ключей, схема БД, `tools:`-блок
промптов — **не трогаются**.
**Норматив на будущее:** любая новая правка/добавление агент-промпта следует этому канону (5 секций +
аддитивная схема + ❌→✅). Отступление требует нового ADR.
## Альтернативы
- **Сразу включить hard-fail схемы.** Отвергнуто: правка `src/config.py` вне scope; для self-hosting
рискованно (забытое поле валит гейт всех проектов). Сначала эмиссия, enforcement — позже.
- **Канон как рекомендация, не норма.** Отвергнуто: теряется машинная проверяемость, эпик требует
контракт.
- **Запечь промпты в образ.** Отвергнуто: противоречит loading-model (cat из worktree), добавило бы
прод-рестарт-зависимость.
## Последствия
- **+** Петля 52 замкнута: схема наполняется реальными данными на каждой стадии всех проектов.
- **+** Единый предсказуемый канон; правки промптов вступают в силу **без прод-рестарта** (следующий
worktree от `main`) → нулевой self-hosting-риск выкатки.
- **+** Естественный in-vivo A/B: reviewer/tester задачи исполняются под новыми промптами в той же
ветке (метод BR-6).
- **** Рост объёма промптов (митигейшн: ссылки вместо инлайна, контроль объёма).
- **** Риск регресса инструкции (митигейшн: построчная карта + структурные тесты + приоритетный
review deployer/reviewer).
- **Откат:** `git revert` PR — свободная форма возвращается, эмиссия прекращается, гейты идентичны.
## Связи
- Реализует: adr-0019 (52b), adr-0020 (52c).
- Per-work-item: `docs/work-items/ORCH-077/06-adr/ADR-001-anthropic-prompt-canon.md`.
- Стандарты: `docs/_standards/PIPELINE_DOCS.md`, `docs/_standards/HANDOFF_PROTOCOL.md`,
`src/frontmatter.py::REQUIRED_FIELDS`.
- Сверено по коду: `src/agents/launcher.py`, `Dockerfile`,
`src/config.py::frontmatter_validation_strict`, `tests/test_agent_frontmatter_no_model.py`.

View File

@@ -1,106 +0,0 @@
---
work_item: ORCH-078
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0022: Стандарт маркеров-трассировки `ORCH-NNN` + правило чтения ADR перед правкой
- **Статус:** proposed
- **Дата:** 2026-06-09
- **Источник:** ORCH-078 (эпик ORCH-52, слой 52e — трассировка, слой 4)
- **Связи:** продолжает цепочку стандартов эпика 52 — adr-0019 (52b, `PIPELINE_DOCS.md`),
adr-0020 (52c, frontmatter-контракт), adr-0021 (52d, канон промптов). Детально —
`docs/work-items/ORCH-078/06-adr/ADR-001-traceability-marker-standard.md`.
## Контекст
Эпик ORCH-52 строит сквозной контракт документации конвейера: **52b** (adr-0019) — описательный
стандарт документов + скелеты; **52c** (adr-0020) — машинный frontmatter-контракт + `HANDOFF_PROTOCOL.md`;
**52d** (adr-0021) — 6 промптов в каноне Anthropic + добровольная эмиссия 52c-схемы. Закрыты слои
структуры (52b), машинного вердикта (52c) и формы промптов (52d), но **слой трассировки кода к
решениям не формализован**.
Факты, сверенные с кодом `main`:
- В `src/` живёт **51 уникальный** маркер `ORCH-NNN` (`grep -rhoE 'ORCH-[0-9]+' src/ | sort -u | wc -l`),
привязывающий нетривиальные инварианты к породившему их work item — **сложившаяся практика без
формального стандарта** (`docs/_standards/` несёт лишь `PIPELINE_DOCS.md`/`HANDOFF_PROTOCOL.md`).
- Высокая плотность: `config.py`=63, `stage_engine.py`=55, `agents/launcher.py`=50, `plane_sync.py`=48,
`merge_gate.py`=26 вхождений.
Три незакрытые проблемы:
1. **Нет правила чтения.** Агент, правя маркированную строку, не обязан прочитать ADR, который её
ввёл → риск молча сломать инвариант. Это класс «фантомного merge»
(`docs/history/LESSONS_2026-06-08_phantom-merge.md`), породившего ORCH-071/073.
2. **Reviewer не контролирует соблюдение** — ось «Соответствие ADR» проверяет ADR текущей задачи, не
сверку правки чужого маркированного кода с его ADR.
3. **Анти-археология** — блок с 50+ маркерами = раскопки по 4+ work item.
## Решение
Ввести **нормативный стандарт маркеров-трассировки** `docs/_standards/TRACEABILITY.md` (слой 4 эпика
52) и точечно дополнить промпты правилом чтения / контролем соблюдения **со ссылкой на единый
источник**. Это **docs + prompts-only**, нулевое касание кода; стандарт — описательно-нормативный
документ + анти-регресс-тест промптов, **не машинный гейт конвейера**.
1. **`TRACEABILITY.md`** кодифицирует существующий контракт (не вводит новый синтаксис): определение
маркера, формат (inline-комментарий, рекомендуется ссылка на решение), правило размещения (рядом с
нетривиальным инвариантом), чтение истории **с реальным проверяемым примером**
(`src/serial_gate.py` → ORCH-088 → `ADR-001-serial-gate.md`), fallback-доступ, анти-археология,
каноничный текст правила чтения.
2. **Единый источник истины правила.** Каноничная формулировка живёт только в `TRACEABILITY.md`;
промпты несут короткую врезку-**ссылку**, не копию → нет дрейфа между файлами (анти-дубль 52d).
3. **Точечные врезки (аддитивно, 52d-канон не переписывается):** `developer.md` — правило чтения +
fallback-доступ («❌ X → ✅ Y»); `architect.md` — правило чтения + анти-археология; `reviewer.md`
усиление оси «Соответствие ADR» под-пунктом «правка маркированного кода сверена с его ADR; слом →
finding ≥P1».
4. **Анти-археология:** блок с **≥3** маркерами → одна сводная ссылка на сквозной ADR
(`docs/architecture/adr/`) вместо перечисления всех work item. Пример: `src/merge_gate.py`
`adr-0006/0013/0014/0016`.
5. **Fallback-доступ:** `git show origin/main:docs/work-items/ORCH-NNN/06-adr/ADR-001-<slug>.md`
когда папки нет в текущей ветке.
6. **Анти-регресс машинно:** расширение `tests/test_agent_prompts_canon.py` (tests-only) — утверждает
присутствие reading-rule/`TRACEABILITY`-маркеров; существующие проверки 52d и
`test_agent_frontmatter_no_model.py` остаются зелёными.
**Границы:** `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, `_parse_*`, `src/frontmatter.py`,
схема БД — **не трогаются**. `frontmatter_validation_strict` остаётся `False`; новый QG не вводится.
Массовый ретро-фит 51 существующего маркера **вне объёма** — стандарт действует «на будущее».
**Норматив на будущее:** новый/правимый значимый инвариант → ставь маркер своей задачи рядом; блок с
3+ маркерами → сводный сквозной ADR; правка чужого маркера → читай его `06-adr` до изменения.
## Альтернативы
- **Машинный гейт/CI-lint маркеров.** Отвергнуто: правка `src/`/CI вне scope; для self-hosting
рискованно (ложный fail валит конвейер всех проектов); премэйчур до описательного стандарта.
Enforcement — потенциальная будущая задача (как hard-fail схемы в adr-0021).
- **Массовый ретро-фит 51 маркера.** Отвергнуто: огромный диф, риск регресса смысла, вне объёма.
- **Копировать правило в каждый промпт.** Отвергнуто: дрейф между файлами, нарушение анти-дубль.
- **Только per-work-item ADR без глобального.** Отвергнуто: рвёт цепочку эпика 52 (52b/c/d имеют
глобальный ADR); нет точки входа для будущих агентов.
## Последствия
- **+** Замкнут слой 4 эпика 52: практика маркеров формализована, правило чтения защищает от слома
инвариантов; reviewer получает ось контроля.
- **+** Единый источник правила → нет дрейфа; обновление в одном файле.
- **+** Self-hosting без рестарта: промпт `cat`-ается из worktree → правило действует на следующем
worktree от `main` без рестарта 8500.
- **+** Полная обратимость: чисто текстовое изменение, нет миграций/состояния/kill-switch.
- **** Рост объёма 3 промптов (митигейшн: короткие врезки-ссылки).
- **** Стандарт нормативен, но не enforced машинно → соблюдение на дисциплине + ревью (осознанный
компромисс).
- **Откат:** `git revert` PR — стандарт удаляется, врезки исчезают, поведение кода/гейтов идентично.
## Связи
- Продолжает: adr-0019 (52b), adr-0020 (52c), adr-0021 (52d).
- Per-work-item: `docs/work-items/ORCH-078/06-adr/ADR-001-traceability-marker-standard.md`.
- Стандарты-соседи: `docs/_standards/PIPELINE_DOCS.md`, `docs/_standards/HANDOFF_PROTOCOL.md`,
будущий `docs/_standards/TRACEABILITY.md` (создаёт стадия development).
- Сверено по коду: `src/serial_gate.py:241,269` (ORCH-088), `src/merge_gate.py` (26 маркеров),
`tests/test_agent_prompts_canon.py`, `.openclaw/agents/{developer,architect,reviewer}.md`.
- Прецедент класса ошибки: `docs/history/LESSONS_2026-06-08_phantom-merge.md`.

View File

@@ -1,98 +0,0 @@
---
work_item: ORCH-079
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0023: Reviewer-ось «обзорные доки» (README-ограничения) + закрытие эпика ORCH-52
- **Статус:** proposed
- **Дата:** 2026-06-09
- **Источник:** ORCH-079 (эпик ORCH-52, слой 52f — обзорные доки, слой 5/финал)
- **Связи:** замыкает цепочку стандартов эпика 52 — adr-0019 (52b, `PIPELINE_DOCS.md`),
adr-0020 (52c, frontmatter-контракт), adr-0021 (52d, канон промптов), adr-0022 (52e,
трассировка маркеров). Детально — `docs/work-items/ORCH-079/06-adr/ADR-001-readme-sync-and-reviewer-overview-docs-axis.md`.
## Контекст
Эпик ORCH-52 строит сквозной контракт «документация = golden source наравне с кодом» слоями:
**52b** (adr-0019) — описательный стандарт документов + скелеты; **52c** (adr-0020) — машинный
frontmatter-контракт + `HANDOFF_PROTOCOL.md`; **52d** (adr-0021) — 6 промптов в каноне Anthropic +
добровольная эмиссия 52c-схемы; **52e** (adr-0022) — стандарт трассировки маркеров + reviewer-ось
«соответствие ADR». Закрыты слои структуры, машинного вердикта, формы промптов и трассировки кода —
но **обзорная витрина проекта (корневой `README.md`) не охвачена**.
Факты, сверенные с кодом `main`:
- Секция `README.md` «Известные ограничения» (`:236241`) имеет битую нумерацию (`1,2,3,4,3,4`) и
**выдаёт решённое за открытое**: worktree-гонки (закрыто `ensure_worktree` + ORCH-026/088),
in-process daemon (закрыто очередью ORCH-1), «Gitea CI не настроен» (опровергнуто `check_ci_green`,
`src/qg/checks.py:82`), «no retry» (опровергнуто backoff/breaker в `queue_worker.py`), плюс
устаревшие issue-ID (зрелый `plane_sync` ORCH-010/066/068) и Playwright-timeout (неприменим к
pytest-сервису; реальный механизм — watchdog ORCH-7).
- **Процессный пробел:** reviewer (ось «Документация») проверяет обновление *конвейерных* доков, но
**обзорные** разделы (README «Известные ограничения») в правиле не названы → витрина копит
рассинхрон, т.к. закрытие ограничения не обязывает автора снять пункт.
## Решение
Закрыть слой 5 (финал) эпика 52: **синхронизировать обзорные доки с кодом по факту** и добавить
reviewer'у **нормативную под-ось «обзорные доки»** (по образцу оси трассировки 52e). Это **docs +
prompt-only**, нулевое касание кода; правило — описательно-нормативное, **не машинный гейт**.
1. **Reviewer-ось «обзорные доки» (cross-cutting).** В `.openclaw/agents/reviewer.md` ось 4
«Документация» + `<constraints>` несут точечную врезку «❌→✅»: *PR закрыл пункт README «Известные
ограничения», README не обновлён → finding*. Severity **≥ P1**; при закрытии ограничения правкой
`src/` без обновления README — совпадает с существующим **P0** «`src/` изменён, доки не обновлены».
Канон 52d (5 секций, формат запретов, `<thinking>`), 6 полей схемы 52c и ключ
`verdict: APPROVED|REQUEST_CHANGES` — байт-в-байт.
2. **Витрина приведена к коду (NFR-3).** Все 6 устаревших пунктов сняты/перенесены в «Закрыто
(история)» с ORCH-ссылками; в «открытых» остаются ТОЛЬКО реально открытые, верифицированные
кодом/задачей; нумерация сквозная. Запрет на изобретение ограничений (только уже
задокументированные known-limitations — анти-scope-creep).
3. **Точечная сверка** `README.md` / `docs/architecture/README.md` с `src/` (стадии/`QG_CHECKS`/
модели-эффорты/компоненты), минимально инвазивно.
4. **Анти-регресс машинно:** расширение `tests/test_agent_prompts_canon.py` (tests-only) — assert
присутствия оси обзорных доков; проверки 52d и `test_agent_frontmatter_no_model.py` зелёные.
**Границы:** `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, `_parse_*`, `src/frontmatter.py`,
схема БД — **не трогаются**. `frontmatter_validation_strict` остаётся `False`; новый QG не вводится.
### Эпик ORCH-52 — закрыт (карта слоёв)
| Слой | Задача | Глобальный ADR | Артефакт |
|------|--------|----------------|----------|
| 52b — структура доков | ORCH-075 | adr-0019 | `docs/_standards/PIPELINE_DOCS.md` + `_templates/` |
| 52c — машинный frontmatter | ORCH-076 | adr-0020 | `src/frontmatter.py` + `HANDOFF_PROTOCOL.md` |
| 52d — канон промптов | ORCH-077 | adr-0021 | 6 промптов `.openclaw/agents/*.md` |
| 52e — трассировка маркеров | ORCH-078 | adr-0022 | `docs/_standards/TRACEABILITY.md` |
| **52f — обзорные доки** (финал) | **ORCH-079** | **adr-0023** | `README.md` + reviewer-ось |
## Альтернативы
- **Машинный enforcement (новый QG «README актуален»).** Отвергнуто: вне scope; для self-hosting
ложный fail валит конвейер всех проектов; правило остаётся нормативным, как 52e. Enforcement —
возможная будущая задача (как hard-fail схемы 52c).
- **Отдельный `docs/_standards/` для правила обзорных доков.** Отвергнуто: одно правило, один
артефакт (README) — врезки в промпт достаточно; новый стандарт-файл избыточен.
- **Только per-work-item ADR.** Отвергнуто: рвёт цепочку эпика 52 (52be имеют глобальный ADR); нет
явной точки «эпик 52 закрыт».
## Последствия
- **+** Витрина проекта честна; самоподдерживающаяся синхронность (reviewer-ось).
- **+** Эпик 52 формально закрыт сквозным ADR — единая точка входа для будущих агентов.
- **+** Self-hosting без рестарта: промпт `cat`-ается из worktree → правило с следующего worktree
от `main` без рестарта 8500.
- **+** Полная обратимость: чисто текстовое изменение, нет миграций/состояния/kill-switch.
- **** Правило нормативно, не enforced машинно → дисциплина + ревью (осознанный компромисс).
- **** Рост `reviewer.md` на короткую врезку (митигейшн: точечность, без переписывания).
- **Откат:** `git revert` PR — доки/промпт/тест откатываются, поведение кода/гейтов идентично.
## Связи
- Замыкает: adr-0019 (52b), adr-0020 (52c), adr-0021 (52d), adr-0022 (52e).
- Per-work-item: `docs/work-items/ORCH-079/06-adr/ADR-001-readme-sync-and-reviewer-overview-docs-axis.md`.
- Сверено по коду: `src/agents/launcher.py` (`ensure_worktree`, `_resolve_timeout`),
`src/queue_worker.py` (backoff/breaker), `src/qg/checks.py:82,381`, `src/plane_sync.py:451,541`,
`README.md:236241`, `.openclaw/agents/reviewer.md`, `tests/test_agent_prompts_canon.py`.
</content>

View File

@@ -1,59 +0,0 @@
---
work_item: ORCH-063
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0024: Disk-watchdog — фоновый heartbeat-демон мониторинга заполнения хост-ФС
> Сквозной (cross-cutting) ADR: вводит **новый фоновый компонент** оркестратора в ряду
> `reconciler` (adr-0007) и `job_reaper` (adr-0011). Детальное решение задачи —
> `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`.
## Статус
Proposed (ORCH-063)
## Контекст
07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь self-hosting-конвейер** (один
прод-инстанс `orchestrator` обслуживает все прод-проекты из общей БД/очереди). Проактивного сигнала
о заполнении диска у системы не было. Оркестратор уже имеет два проверенных фоновых daemon-потока с
единым каркасом (`threading.Thread(daemon=True)` + `threading.Event`, `start/stop/status`,
never-raise, снимок в `GET /queue`): `reconciler` (ORCH-053) и `job_reaper` (ORCH-065). Новый
эксплуатационный watchdog логично встроить тем же паттерном.
## Решение
Вводится третий фоновый компонент **disk-watchdog** (`src/disk_watchdog.py`):
- **Калька каркаса** `reconciler`/`reaper`: daemon-поток, чистый stop через `_stop.wait(interval)`,
контракт `start()`/`stop(timeout)`/`status()`, старт/стоп в `main.lifespan` (старт последним —
после `reaper.start()`; стоп первым в reverse-порядке), наблюдаемость — аддитивный блок
`disk_monitor` в `GET /queue`.
- **Замер** заполнения **хост-ФС** через смонтированные bind-пути (`/repos`, `/app/data`) stdlib
`shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`); дедуп путей по `st_dev`.
- **Решение об алерте** — pure-функция от `(used_pct, threshold, prev_state, now, realert_s)`:
алерт на пересечении порога (дефолт 85%), ограниченный cooldown-повтор, recovery при возврате
ниже порога. Состояние анти-спама — in-memory (без миграции БД).
- **Алерт** — `send_telegram` (notifying), best-effort. Kill-switch `disk_monitor_enabled`.
- **Только сигнал, не лечение:** watchdog читает и уведомляет, не трогает диск/контейнер, не
рестартит прод (self-hosting безопасность). Авто-очистка диска — отдельная задача.
**Инварианты:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, схема БД — **не меняются**
(watchdog — эксплуатационный демон, не Quality Gate, как `reconciler`/`reaper`). never-raise на
уровнях per-path / per-tick / per-send. При выключенном kill-switch — поведение 1:1 как сейчас
(нулевая регрессия для enduro-trails).
## Последствия
- **+** Ранний сигнал предотвращает групповой простой всех проектов; дёшево, без внешних
зависимостей (принцип «всё в Docker на одном сервере, минимум зависимостей»).
- **+** Знакомый паттерн фонового демона → низкий риск, простое сопровождение.
- **** In-memory состояние / best-effort Telegram — допустимы для раннего сигнала (не SLA).
- **Откат:** `ORCH_DISK_MONITOR_ENABLED=false`; миграций БД нет.
## Ссылки
- Задачный ADR: `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`
- Родственные компоненты: [adr-0007-reconciler.md](adr-0007-reconciler.md),
[adr-0011-job-reaper-lease-reclaim.md](adr-0011-job-reaper-lease-reclaim.md)
- Топология host-разделов: `docs/operations/INFRA.md`
</content>

View File

@@ -1,86 +0,0 @@
---
work_item: ORCH-062
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0025: Build-cache-pruner — фоновый heartbeat-демон авто-уборки docker build cache на хосте
> Сквозной (cross-cutting) ADR: вводит **новый фоновый компонент** оркестратора в ряду
> `reconciler` (adr-0007), `job_reaper` (adr-0011) и `disk_watchdog` (adr-0024). Детальное
> решение задачи — `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`.
## Статус
Proposed (ORCH-062)
## Контекст
07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь self-hosting-конвейер всех
проектов** (один прод-инстанс `orchestrator` на общей БД/очереди). Доминирующий «пожиратель» —
**docker build cache** (≈11 ГБ от частых пересборок прод/staging-образов). `disk_watchdog`
(adr-0024, ORCH-063) ввёл **сигнал** о заполнении (Telegram ≥85%) и явно отложил авто-очистку в
отдельную задачу. ORCH-062 — эта задача: **автоматическое освобождение build cache**, чтобы
инцидент не повторялся без оператора.
Сверено по коду: контейнер `orchestrator` **не содержит docker CLI** (`Dockerfile:11` — только
`openssh-client git curl`); host-docker-операции приложение уже делает **через ssh на хост**
(`image_freshness.image_revision`, `self_deploy` Phase B), канал `deploy_ssh_user@deploy_ssh_host`
настроен. У оркестратора три проверенных фоновых daemon-потока с единым каркасом.
## Решение
Вводится четвёртый фоновый компонент **build-cache-pruner** (`src/build_cache_pruner.py`):
- **Калька каркаса** `disk_watchdog`/`reconciler`/`reaper`: daemon-поток, чистый стоп через
`_stop.wait(interval)`, контракт `start()`/`stop(timeout)`/`status()`, старт/стоп в
`main.lifespan` (старт последним — после `disk_watchdog.start()`; стоп первым в reverse),
наблюдаемость — аддитивный блок `build_cache_prune` в `GET /queue`. Leaf-модуль (без обратных
зависимостей на `stage_engine`/`stages`/`qg`).
- **Уборка — строго `docker builder prune -f --filter until=<until>`** (BuildKit GC, дефолт
`until=24h`): удаляется только старый build cache, тёплый ≤24ч сохраняется. `-a` — опционально и
только в паре с возрастным фильтром. **Запрещены** `docker image prune`/`system prune`/удаление
образов запущенных сервисов/остановка-рестарт контейнеров.
- **Исполнение на хосте через ssh** (CLI в контейнере нет): `ssh deploy_ssh_user@deploy_ssh_host
"docker builder prune …"`, bounded таймаутом. **Нет ssh-таргета → тик no-op** → фича
естественно скоупится на self-hosting-прод.
- **Конфиг/kill-switch** (`ORCH_BUILD_CACHE_PRUNE_*`, дефолты безопасные): `enabled` (дефолт
`true`), `interval_s` (6ч), `until` (`24h`), `all` (`false`), `timeout_s`, `notify_min_gb`.
Валидаторы по образцу `disk_monitor_*` (невалид → лог + дефолт).
- **Сигнал + лечение как пара:** disk_watchdog сигналит о росте диска, build-cache-pruner убирает
доминирующего «пожирателя» — две половины одной операционной защиты.
**Инварианты:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, `src/stage_engine.py`, схема БД
— **не меняются** (pruner — эксплуатационный демон, не Quality Gate, как watchdog/reaper). Без
миграции БД (учёт результата in-memory, best-effort). never-raise per-команда/per-tick. Уборка
**никогда** не рестартит docker daemon/прод-контейнер (self-hosting безопасность; рестарт-путь —
отвергнутый Вариант B). При выключенном kill-switch — поведение 1:1 как сейчас (нулевая регрессия
для enduro-trails).
## Альтернативы
- **host `daemon.json builder.gc.defaultKeepStorage`** — отвергнуто: требует рестарта docker
daemon (останавливает ВСЕ контейнеры хоста = групповой self-hosting риск); политика по объёму,
не по возрасту; не наблюдаемо в `GET /queue`.
- **host-cron** — отвергнуто как основное (оставлено ручным fallback): off-git невидимая инфра,
без `/queue`-наблюдаемости, без config-kill-switch, не тестируется.
- **raw-HTTP по docker.sock / docker CLI в образе** — отвергнуто: лишний код / раздувание образа
против уже существующего ssh-канала.
## Последствия
- **+** Корень инцидента 07.06 устраняется автоматически; тёплый кэш сохранён; без новых
зависимостей и без рестарта docker/прода (принцип «всё в Docker, минимум зависимостей»).
- **+** Знакомый паттерн фонового демона → низкий риск, наблюдаемость, обратимость, тестируемость.
- **** Зависимость от ssh на хост (как `image_freshness`/`self_deploy`); нет таргета → no-op
(наблюдаемо), фича не работает, но ничего не ломает.
- **Откат:** `ORCH_BUILD_CACHE_PRUNE_ENABLED=false`; миграций БД нет.
## Ссылки
- Задачный ADR: `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`
- Инфра/риски: `docs/work-items/ORCH-062/07-infra-requirements.md`,
`docs/work-items/ORCH-062/10-tech-risks.md`
- Комплемент: [adr-0024-disk-watchdog.md](adr-0024-disk-watchdog.md) (ORCH-063 — сигнал)
- Родственные компоненты: [adr-0007-reconciler.md](adr-0007-reconciler.md),
[adr-0011-job-reaper-lease-reclaim.md](adr-0011-job-reaper-lease-reclaim.md)
- Топология host / env-карта: `docs/operations/INFRA.md`
</content>

View File

@@ -1,106 +0,0 @@
---
work_item: ORCH-090
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# ADR-0026: Системное терминальное состояние `cancelled` — STOP-отмена задачи
Сквозной (cross-cutting) ADR. Детальное решение задачи —
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`.
## Статус
Proposed
## Контекст
ORCH-090 вводит Plane-статус **STOP** — единый декларативный механизм отмены задачи (остановка
агента + полный сброс прогресса). Самое́ кросс-каттинговое следствие — появление **нового
системного терминального состояния `cancelled`** (стадия `tasks.stage='cancelled'` + терминальный
job-статус `jobs.status='cancelled'`). До ORCH-090 «терминальность задачи» в горячем планировщике
была захардкожена как **`stage == 'done'`** (единственный сток в `STAGE_TRANSITIONS`), и это
определение разъехалось между подсистемами:
- `src/reconciler.py` **уже** трактует `stage in ("done","cancelled")` как терминал-скип
(ORCH-086 D2 предвосхитил `cancelled`; стр. 196) и `_is_terminal_state` по группе Plane
`{completed, cancelled}` (ORCH-068, стр. 398415).
- `src/serial_gate.py` (ORCH-088) и `src/task_deps.py` (ORCH-026) считают задачу «незавершённой»
по `stage != 'done'`**без** `cancelled`. Если ввести `cancelled`-стадию, не тронув их,
отменённая задача навсегда будет «активной»/«незавершённой зависимостью» и **заклинит очередь
репо**.
Этот ADR фиксирует `cancelled` как первоклассное терминальное состояние, равноправное `done`, и
перечисляет ВСЕ точки, где системный предикат терминальности должен его признавать.
## Решение
### Инвариант
**«Задача терминальна» ⇔ `stage ∈ {done, cancelled}`.** Это единое определение для всех
подсистем планировщика/мониторинга. `cancelled` — терминальный **сток** (не новое ребро
конвейера): exit-гейты рёбер `STAGE_TRANSITIONS` и реестр `QG_CHECKS`/`check_*` **не меняются**.
### Точки, признающие `cancelled` терминальным (исчерпывающе)
1. `src/stages.py::STAGE_TRANSITIONS` — добавить сток
`"cancelled": {"next": None, "agent": None, "qg": None}` (параллельно `done`).
2. `src/serial_gate.py``repo_has_other_unfinished` и claim-фрагмент `t2.stage != 'done'`,
snapshot: `stage != 'done'``stage NOT IN ('done','cancelled')`. **(маркер ORCH-088)**
3. `src/task_deps.py` — dep-gate и `is_task_ready`: `stage != 'done'`
`stage NOT IN ('done','cancelled')`. **(маркер ORCH-026)**
4. `src/reconciler.py` — уже покрыто скипом `stage in ("done","cancelled")` (стр. 196);
`get_active_tasks_for_reconcile` опционально сузить до `NOT IN ('done','cancelled')`.
5. `src/job_reaper.py` / `src/queue_worker.py` — перед авто-requeue dead/running-job'а сверять
терминал задачи: `stage in ("done","cancelled")` → job помечается `cancelled`, не реквью'ится.
6. `src/post_deploy.py` / `stage_engine.run_post_deploy_monitor` — монитор не тикает по
отменённой задаче (терминал-проверка/маркер `done`).
### Новые терминальные исходы
- **Job:** `jobs.status='cancelled'` — нигде не реквью'ится; `claim_next_job` выбирает только
`status='queued'` (изменений в claim нет). `mark_job` стампит `finished_at` для `cancelled`.
- **Задача:** `tasks.stage='cancelled'` + аддитивные колонки `cancelled_at`,
`cancel_requested_at` (отложенная отмена в критическом окне merge/deploy). Натуральные ключи
`plane_id`/`work_item_id` тумбстонятся (`#cancelled-<id>`) для переиспользования «To Analyse»
с нуля; `plane_issue_id` сохраняется (аудит). Детали — 08-data-requirements.md.
### Точки врезки STOP (компоненты)
- `plane.py` — маршрут `stop` (fail-closed, не в `_DEFAULT_STATES`) → `handle_stop`; гейт релонча
ограничен стадией `analysis`.
- `stage_engine.cancel_task` — оркестрация отмены (graceful SIGTERM, cancel-jobs, worktree+branch,
tombstone, notify); безопасное прерывание merge/deploy (D7 локального ADR).
- leaf `src/cancel.py` — чистая логика (`applies`/`in_critical_window`/`snapshot`), never-raise.
- `src/gitea.py``delete_remote_branch` (never-raise; только feature-ветка, `main` неприкосновенен).
- `GET /queue` — read-only блок `stop`.
### Флаги / совместимость
- Kill-switch `stop_status_enabled` + scope `stop_status_repos` (CSV, пусто → все репо).
- При `stop_status_enabled=False`: STOP-обработка и гейт релонча инертны; расширение
терминал-набора `cancelled` безвредно при отсутствии отменённых задач → **нулевая регрессия**.
- `STAGE_TRANSITIONS` (exit-гейты) / `QG_CHECKS` / `check_*` / семантика
Approved/Rejected/Confirm Deploy / merge-gate (ORCH-043) / merge-verify (ORCH-071/073) /
image-freshness (ORCH-058) / post-deploy (ORCH-021) / serial-gate FIFO (ORCH-088) / auto-label
(ORCH-089) — **без изменений**.
- Миграции БД — только аддитивные/идемпотентные (`_ensure_column`); enduro не затронут (NFR-2).
## Последствия
- **+** Единое, консистентное определение терминальности — устранён латентный рассинхрон
`done`-only между планировщиком и реконсилятором.
- **+** STOP безопасен для self-hosting: не трогает `main`/прод, отложенная отмена в критическом
окне.
- **** Терминальность теперь читается из набора `{done, cancelled}`, а не из скаляра `'done'`
будущие подсистемы обязаны использовать набор. Митигейшн: этот ADR + маркер `ORCH-090` в
изменённых местах + тесты.
- **Откат:** `stop_status_enabled=False`; полный revert — снять врезки и вернуть предикаты к
`stage != 'done'`.
## Эволюция маркеров `cancelled`-терминала
Места, признающие `cancelled` терминальным (см. список выше), несут маркер `ORCH-090`. Правка
любого из них — сверяться с этим ADR (анти-археология: 3+ маркеров → одна ссылка сюда,
TRACEABILITY.md).
## Ссылки
- Детальный ADR: `docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`
- Data: `docs/work-items/ORCH-090/08-data-requirements.md`
- Связанные: adr-0017 (serial-gate), adr-0015 (task-deps), adr-0007 (self-deploy),
adr-0006 (merge-gate), adr-0018 (auto-label)

View File

@@ -1,82 +0,0 @@
---
work_item: ORCH-093
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0027: Merge-актор — ретрай транзиентных ошибок Gitea + гард «ветка уже в `main`»
Сквозной (cross-cutting) ADR. **Амендмент** к [adr-0013](adr-0013-merge-verify-gate.md) (merge-verify
под-гейт), [adr-0014](adr-0014-merge-verify-sha-source-of-truth.md) (SHA-в-main как источник истины)
и [adr-0016](adr-0016-ensure-open-pr-before-merge-verify.md) (гарантированный код-PR). Детальное
решение задачи — `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`.
> Регистрируется как сквозной, т.к. правит блок merge-актора с **3+ маркерами** (`ORCH-071`,
> `ORCH-073`, `ORCH-082`) — анти-археология маркеров (`docs/_standards/TRACEABILITY.md`): сводный
> ADR агрегирует эволюцию вместо перечисления work item в коде.
## Статус
Proposed
## Контекст
Детерминированный merge-актор merge-verify под-гейта (`deploy → done`, self-hosting) состоит из
`ensure_open_pr``merge_pr``verify_merged_to_main` (`src/merge_gate.py`). Инцидент **ORCH-063
(09.06)** вскрыл два дефекта, оба сверены по коду прода:
1. `merge_pr`**one-shot**: `POST /pulls/{index}/merge`, любой не-`200/201` → мгновенный `False`.
Транзиентная икота Gitea (`405 "Please try again later"` при пересчёте `mergeable` сразу после
пуша; `5xx`; таймаут) → ложный HOLD защиты ORCH-071/073 → ручной домерж.
2. `ensure_open_pr` — после ручного мержа код-PR `closed`, открытый не найден → создаёт **новый
пустой PR** на ветке, уже целиком в `main`.
Защита ORCH-071/073 («deploy succeeded but not merged») корректна и сохраняется; задача снижает
лишь **ложные** срабатывания на транзиентах и устраняет мусорные PR. Это блокер автономного прогона
(эпик ORCH-088).
## Решение
Аддитивно, без правки `STAGE_TRANSITIONS` / `QG_CHECKS` / схемы БД; INV-4 (мерж только через Gitea
PR-merge API; никогда `push`/`force-push` в `main`) и never-raise сохранены.
- **Ретрай-loop вокруг `POST …/merge`** (только мутирующий вызов) до `merge_retry_max_attempts`
(дефолт 3) с экспоненциальным backoff и потолком (`base 2`, `max 5`; суммарно ≤10 с). Классификатор
**транзиент** (`405`/`408`/`5xx`/таймаут/сетевое; `409`/`422` при `mergeable==True`; `mergeable==None`
→ транзиент-по-дефолту в рамках бюджета) vs **терминал** (`403`/`404`; `409`/`422` при
`mergeable==False`) — по коду ответа **и** полю `mergeable` (`GET /pulls/{index}`). Терминал →
быстрый честный `False` (защита ORCH-071/073 — как прежде). Образец — `check_ci_green`
(`attempt i/N`) + transient-breaker агентов.
- **Гард already-in-main в `ensure_open_pr`**: перед созданием PR — `git merge-base --is-ancestor
<branch> origin/main` (rc==0 → ветка целиком в `main`) → новый исход `"already-in-main"`, PR не
создаётся; git-ошибка/ambiguous → **fail-OPEN** на текущий create-путь (гард не должен превратить
икоту git в ложный no-op мержа). `_handle_merge_verify` трактует `"already-in-main"` как «мержить
нечего» → пропуск `merge_pr` → авторитетный SHA-в-main (`verify_merged_to_main`, ADR-0014) доводит
до `done` без мусорного PR.
- **Конфиг**: `merge_retry_enabled` (kill-switch; `False` → one-shot, нулевая регрессия),
`merge_retry_max_attempts`, `merge_retry_backoff_base_s`, `merge_retry_backoff_max_s`
(env `ORCH_MERGE_RETRY_*`). Гард already-in-main — без отдельного флага (накрыт существующим
`merge_verify_autocreate_pr_enabled`).
Объём раската — реально только self-hosting (`merge_verify_applies`); на прочих репо мерж делает
LLM-deployer → изменение нейтрально.
## Последствия
- **+** Транзиент Gitea переживается автоматически → нет ложного HOLD / ручного домержа в автономном
конвейере; нет мусорных пустых PR; повтор финализатора идемпотентен.
- **+** Реальный конфликт → быстрый честный HOLD; защита ORCH-071/073 и SHA-в-main (ADR-0014) —
авторитетны и неизменны.
- **** Дефолт `mergeable==None → transient` может добавить ≤10 с до HOLD на реальном конфликте
(бюджет жёстко ограничен); один лишний `GET /pulls/{index}` в редком ambiguous-кейсе.
- **Откат:** `ORCH_MERGE_RETRY_ENABLED=false` → one-shot; `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=false`
→ отключает врезку `ensure_open_pr` с гардом. Полный откат — revert PR.
## Ссылки
- Детальный ADR: `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`
- Лехатая: [adr-0006](adr-0006-merge-gate.md), [adr-0013](adr-0013-merge-verify-gate.md),
[adr-0014](adr-0014-merge-verify-sha-source-of-truth.md),
[adr-0016](adr-0016-ensure-open-pr-before-merge-verify.md)
- Код: `src/merge_gate.py`, `src/stage_engine.py::_handle_merge_verify`, `src/config.py`

View File

@@ -1,96 +0,0 @@
---
work_item: ORCH-094
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# adr-0028: Terminal-window-aware гард выставления deploy-фазовых статусов Plane
Сквозной (cross-cutting) ADR. **Амендмент** к [adr-0010](adr-0010-post-deploy-monitor.md)
(post-deploy monitor, ORCH-021) и Plane-статусной модели (ORCH-066): вводит инвариант
«deploy-фазовые Plane-статусы — terminal-window-aware» поверх общих сеттеров `plane_sync` и
переупорядочивает блок `next_stage == "done"` в `advance_stage`. Детальное решение задачи —
`docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`.
> Регистрируется как сквозной, т.к. правит **общие** сеттеры `set_issue_awaiting_deploy`/
> `set_issue_deploying`/`set_issue_monitoring` (используются системно) и трогает маркированный блок с
> `ORCH-021`/`ORCH-066` (`docs/_standards/TRACEABILITY.md`).
## Статус
Proposed
## Контекст
Терминальная (`done`) задача в Plane **не держит `Done`**: непрерывный флапп
`Awaiting Deploy ⟷ Monitoring after Deploy` (верифицировано живьём на **ORCH-061**, task 47, done с
07.06 — 273 активности, само не затихает). Установлено по коду/логам/БД прода:
- Три code-писателя deploy-фазовых статусов (`src/stage_engine.py:404/1218/1316`) делегируют в тонкие
сеттеры `src/plane_sync.py`, которые **БД-стадию не читают** ⇒ терминал-слепы: любой повторный вызов
перезаписывает `Done` обратно на промежуточный статус.
- **Ordering:** `update_task_stage("done")` (`stage_engine.py:369`) пишет `tasks.stage='done'`
**раньше** легитимного `set_issue_monitoring` (стр. 404) ⇒ пост-деплой-окно ORCH-021 — by-design
индикация поверх уже-`done` задачи. Наивный гард «stage==done → Done» ⇒ регресс легитимного окна.
- Актор всех 273 переходов — бот-токен орка (`daf4d3f4-…`), не привязан к активной task/job; в БД нет
активного post-deploy-monitor для task 47 (окно 15 мин закрыто). Реконсилятор F-1 пропускает
`done`/`cancelled`, F-2 опрашивает только `[to_analyse, approved, rejected]` ⇒ механизма привести
застрявшую на deploy-статусе done-задачу к `Done` нет.
## Решение
**Единый terminal-window-aware гард на низком чокпоинте** — на входе трёх deploy-фазовых сеттеров
`plane_sync`. Чистую логику держит **новый leaf-модуль `src/deploy_status_guard.py`** (never-raise,
config-gated; образец `serial_gate.py`/`labels.py`/`cancel.py`); сеттеры исполняют вердикт.
- **Инвариант легитимности:** deploy-фазовый статус легитимен ⇔ задача **нетерминальна** ИЛИ
(`done` **И** активно пост-деплой-окно). Иначе — идемпотентное схождение к `Done`.
`decide(work_item_id, target) -> ALLOW | CONVERGE_DONE | SUPPRESS`:
kill-switch off / чужой issue / не-self репо / нетерминал → **ALLOW**; `cancelled`**SUPPRESS**;
`done` + `target==monitoring` + `window_active`**ALLOW**; `done` иначе → **CONVERGE_DONE**
(`set_issue_done`, идемпотентно); любое исключение → **ALLOW** + warning (never-raise).
- **Новый helper** `post_deploy.window_active(repo, wi)` = `has_marker(ARMED) and not
has_marker(DONE)` (restart-safe).
- **Перенос арм-блока** (`post_deploy.arm_monitor`) **перед** terminal-sync в блоке
`next_stage == "done"`: на стр. 404 `ARMED` уже записан ⇒ `window_active==True` ⇒ легитимный первый
`Monitoring` проходит; re-drive после закрытия окна сходится к `Done`.
- **Харднинг монитора:** идемпотентный страж `has_marker(...DONE)` (ранний return без PATCH/реэнкью)
+ тик no-op при `cancelled` мид-окно; тики привязаны к активному job'у (нет job → нет тика).
- **Наблюдаемость:** каждый вердикт логируется (`work_item`/`caller`/`target`/`db_stage`/
`window_active`/вердикт); подавление/схождение — явно.
- **Флаги** (`config.py`): `deploy_status_guard_enabled=True`
(`ORCH_DEPLOY_STATUS_GUARD_ENABLED`, kill-switch → 1:1) + `deploy_status_guard_repos=""`
(`ORCH_DEPLOY_STATUS_GUARD_REPOS`, пусто → self-hosting only) с локальным `applies(repo)`.
## Альтернативы
- **Гард в caller'ах `stage_engine`** — отвергнуто: не ловит неизвестный/стейл путь под бот-токеном,
размазывает инвариант.
- **Наивный «stage==done → Done» без предиката окна** — отвергнуто: регресс легитимного `Monitoring`.
- **Bypass-флаг на доверенном вызове 404** — отвергнуто в пользу переноса арм-блока (один предикат).
- **Активная сходимость в реконсиляторе F-2** — отвергнуто как основной механизм (лишний polling,
правка маркированного F-2); гард на сеттере гасит непрерывный флапп.
## Последствия
- Терминальная задача стабильно держит `Done`; маятник гаснет за один цикл независимо от актора.
- Легитимный пост-деплой `Monitoring` и рабочий self-deploy-цикл — 1:1 (предикат окна + перенос арм).
- `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / схема БД — **не тронуты**.
- `main`/force-push/прод-контейнер/detached-деплой — не тронуты; не-self репо инертны.
- Ограничение: если актор флаппа — внешняя Plane-automation (вне кода орка), гард — буфер на стороне
орка; локализация (FR-1) и итог документируются (BR-7).
- **Откат:** `ORCH_DEPLOY_STATUS_GUARD_ENABLED=false` → поведение 1:1; полный — revert ветки.
## Связи
- [adr-0010](adr-0010-post-deploy-monitor.md) (ORCH-021 — пост-деплой-окно, sentinel `armed`/`done`,
арм-блок) — амендмент: окно становится предикатом легитимности `Monitoring`.
- ORCH-066 (Plane-статусная модель — слой B индикации; `deploy→done` self ⇒ `Monitoring`) — инвариант
сохранён.
- [adr-0026](adr-0026-stop-cancel-task.md) (ORCH-090 — терминал `cancelled`) — гард не штампует
deploy-статус поверх `cancelled`.
- ORCH-068/086 (терминал-скип реконсилятора) — этот ADR распространяет идею терминал-aware на
выставление deploy-статусов.
- Детально: `docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`.

View File

@@ -1,92 +0,0 @@
---
work_item: ORCH-027
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0029: Гейт покрытия тестами — edge sub-gate + ratchet-базовая линия
- **Статус:** proposed
- **Дата:** 2026-06-10
- **Задача:** ORCH-027
- **Детальный ADR:** `docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`
## Контекст
Оркестратор автономен: `developer` пишет код без человека-фильтра, `tester` сам решает, хватает
ли тестов. Существующие тестовые гейты судят только по факту прохождения, не по полноте:
`check_ci_green` (exit-code CI), `check_tests_passed` (LLM-вердикт `tester`'а), merge-gate
re-test (exit-code). Ни один не замечает «300 строк кода, 0 тестов». При пакетном автономном
прогоне (ORCH-088) это монотонная деградация покрытия. Нужна детерминированная метрика — по духу
как security-гейт (adr-0012).
## Решение
Детерминированный (без LLM) **гейт покрытия как под-гейт ребра `deploy-staging → deploy`**,
рядом с security-gate (ORCH-022), merge-gate (ORCH-043), image-freshness (ORCH-058). Паттерн —
leaf-модуль `src/coverage_gate.py` (never-raise) + обёртка в `QG_CHECKS` (`check_coverage_gate`)
+ врезка `_handle_coverage_gate` в `advance_stage`. `STAGE_TRANSITIONS` не меняется.
- **Порядок: security → merge → `coverage` → image-freshness.** Coverage идёт **ПОСЛЕ
merge-gate** (ветка догнана на свежий `origin/main` → меряем покрытие того кода, что landed) и
**ДО image-freshness** (фейлить дёшево до docker-rebuild). На этой точке merge-lease **held**
**FAIL обязан освободить lease** при откате (как image-freshness rollback; в отличие от
security, который идёт до захвата lease).
- **Измеритель:** `pytest-cov` (`coverage.py`), `python -m pytest tests/ --cov=src
--cov-report=json` в изолированном worktree (`ensure_worktree`); метрика —
`totals.percent_covered`. Тайм-аут `coverage_run_timeout_s`. Скоуп — `src/` (не тесты).
- **Чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)`:
`absolute` (≥floorε), `baseline` (≥baselineε, ratchet), `both` (дефолт). `baseline=None` →
bootstrap (только absolute). FAIL → откат на `development` + developer-retry (cap
`MAX_DEVELOPER_RETRIES`), дословный reason в `task_desc` (ORCH-046).
- **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha,
updated_at)` (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/`job_deps`). Выбор БД над
файлом-в-репо: нет git-churn/конфликтов на ratchet, restart-safe, атомарное обновление.
- **Ratchet-up** в choke-point подтверждённого merge `_handle_merge_verify` (ребро
`deploy → done`, ORCH-071/073): читает измеренное покрытие из `18-coverage-report.md`,
атомарный compare-and-set `UPDATE ... WHERE coverage <= measured` (базовая линия не падает).
Под held merge-lease + per-repo сериализацией merge (ORCH-043) — двойная анти-гонка.
- **Артефакт `18-coverage-report.md`** с frontmatter `coverage_status: PASS|FAIL` (+
`measured_coverage`/`baseline`/`floor`/`policy`/`delta` + аддитивная 52c-схема); вердикт
читается ТОЛЬКО из frontmatter через `src/frontmatter.py` (single source of truth).
- **Условность (как ORCH-35/43/58):** `coverage_gate_enabled` + `coverage_gate_repos` (пусто →
только self-hosting `orchestrator`); вне области → no-op pass. `applies(repo)` ПЕРВОЙ, дорогой
прогон — только при applies.
- **Ошибка инструмента → fail-open + WARNING** по умолчанию (`coverage_tool_fail_closed=False`,
анти-петля как ORCH-061); флаг → fail-closed.
- **Наблюдаемость:** read-only блок `coverage` в `GET /queue`; FAIL → Telegram (кликабельный
номер, измеренное/порог/дельта). Опциональный `POST /coverage/baseline` (ручной override).
- **never-raise**, гейт не деплоит/не рестартит прод/не пушит в `main` (NFR-3).
## Альтернативы
- **CI-job (`check_ci_green`):** пороги/политика/baseline/артефакт плохо выражаются статусом
коммита; ratchet требует записи в БД. Отклонено для v1 (точка расширения).
- **Edge `testing → deploy-staging`:** ветка не догнана на свежий `main` → метрика неточна;
откат не освобождает lease. Отклонено.
- **Базовая линия в файле репо:** git-churn/конфликты на каждый ratchet. Отклонено.
- **Новая стадия `coverage`:** «пустая» стадия без агента не имеет триггера (как ORCH-043/022).
Отклонено.
- **Жёсткий absolute-порог без baseline/epsilon:** массовые ложные заворота. Отклонено.
## Последствия
- Класс «тихо просевшее покрытие» закрыт детерминированной метрикой; baseline только растёт.
- Нулевая регрессия вне области (enduro-trails); `STAGE_TRANSITIONS`/`QG_CHECKS`-семантика/
вердикт-ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`) —
байт-в-байт прежние; новая БД-таблица аддитивна.
- Плата: ещё один «скрытый» под-гейт ребра; новая pip-зависимость (`pytest-cov`); доп. прогон
pytest (после merge-gate re-test, ограничен таймаутом, фейлит до rebuild); v1 — Python-only.
- Дефолтный fail-open тихо пропускает при устойчивом сбое инструмента (с WARNING) —
переключаемо `coverage_tool_fail_closed`.
- Сквозное изменение (новый QG + edge-под-гейт + новая таблица + новый артефакт) →
`arch:major-change`; прод-деплой строго через staging-гейт (8501), без рестарта прод-контейнера.
- **Откат:** `coverage_gate_enabled=False` → полный no-op (мгновенный обратимый kill-switch).
## Связи
adr-0012 (security-гейт — паттерн edge-под-гейта/leaf/never-raise/fail-open), adr-0006
(merge-gate — edge-под-гейт/откат/merge-lease), adr-0008 (image-freshness — условность/
fail-closed/release-lease-on-rollback), adr-0003 (условный гейт / `is_self_hosting_repo`),
adr-0009 (анти-петля ложных FAIL, ORCH-061), adr-0013/adr-0014 (merge-verify / SHA-in-main как
source of truth — точка ratchet), adr-0015/adr-0017 (per-repo сериализация merge/serial-gate),
adr-0020 (frontmatter-контракт — парсинг `coverage_status:`), adr-0019 (PIPELINE_DOCS — артефакт
`18-coverage-report.md`), ORCH-9/15 (мульти-стек — будущая зависимость BR-6).

View File

@@ -1,88 +0,0 @@
---
work_item: ORCH-099
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0030: Лёгкий read-only `/metrics` — сырьё о самом орке для sidecar (F1b)
- **Статус:** proposed
- **Дата:** 2026-06-10
- **Задача:** ORCH-099 (FND/F1a)
- **Детальный ADR:** `docs/work-items/ORCH-099/06-adr/ADR-001-metrics-endpoint.md`
## Контекст
Эпик автономного саморазвития, домен 0 «Фундамент». Рамка наблюдаемости (заказчик): **наблюдатель
отделён от наблюдаемого** — мозг мониторинга (пороги/алерты/история/Telegram) живёт в отдельном
sidecar-контейнере **F1b** (`watchdog/`), а орк отдаёт **только сырьё**, которое знает лишь он сам.
Сегодня такого источника нет: `/health` = `{"status":"ok"}`, `/status` = активные задачи, `/queue`
«человеческий» снимок, перемешанный с конфигом демонов. Нет стабильного машинного контракта для
детекта застрявшей стадии / зависшего агента / деградации очереди / всплеска стоимости. F1b
заблокирована этой задачей. Self-hosting: прод общий с enduro-trails ⇒ эндпоинт обязан быть строго
read-only и never-raise.
## Решение
Новый **leaf-модуль** `src/metrics.py` (`build_metrics() -> dict`, чистый, never-raise по разделам —
паттерн `serial_gate.snapshot()`) + тонкий эндпоинт `@app.get("/metrics")` в `src/main.py` (стиль
`GET /queue`). Только чтение существующих таблиц (`tasks`/`jobs`/`agent_runs`) и in-memory-снапшотов
+ два read-only helper'а в `src/db.py`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-
ключи/схема БД — **не трогаются**.
- **Конверт + контракт версии:** `schema_version` (старт `1`), `generated_at` (UTC ISO-8601 —
момент снимка, домен часов орка), `clk_tck` (`os.sysconf("SC_CLK_TCK")`), разделы
`stages`/`queue`/`agents`/`cost`. **Политика версии:** аддитивные изменения НЕ бампят (sidecar
обязан игнорировать незнакомые ключи и толерировать отсутствие опциональных); бамп — только при
ломающем (rename/remove/retype). Forward-compatible контракт для F1b.
- **`stages`** — `db.get_active_tasks_for_reconcile()` + фильтр `stage NOT IN ('done','cancelled')`
на слое metrics (helper намеренно отдаёт `cancelled` для ORCH-086 — не трогаем его инвариант);
поля `work_item`/`stage`/`age_in_stage_s`/`repo`.
- **`queue`** — `db.job_status_counts()` (+`cancelled`), глубина, сырьё ретраев
(`attempts`/`max_attempts`/`transient_attempts`/в-backoff), `worker.breaker.snapshot()`,
`max_concurrency`. Недоступный worker → `breaker: null`, не 500.
- **`agents` (liveness)** — новый dedicated read-only helper `db.get_running_agents()` (НЕ расширение
hot-path `get_running_jobs()` reaper'а, ORCH-065): `agent`/`run_id`/`job_id`/`pid`/`runtime_s`
(= `running_age_s` от `jobs.started_at`)/`model`/`effort`. CPU-сырьё — **вариант A**: орк читает
`/proc/<pid>/stat` (поля 14+15, utime+stime) → `cpu_ticks`; **дельту не считает** — арбитр
«жив/завис» это sidecar (stateless-эмиссия). `pid is None`/мёртвый/нет `/proc`/не-Linux →
`cpu_ticks: null`, не ошибка.
- **`cost`** — `running` (по running-job, часто `null` до завершения — честное сырьё, `null` ≠ ноль)
+ `aggregate` (новый helper `db.agent_cost_totals()`, `COALESCE(SUM(...),0)` по
`cost_usd`/`input_tokens`/`output_tokens`/`cache_read_tokens`/`cache_creation_tokens`).
- **Kill-switch** `metrics_endpoint_enabled` (env `ORCH_METRICS_ENABLED`, дефолт `True`): при `False`
`200` с `{"schema_version":1,"enabled":false}` (контракт остаётся парсимым). Операторский
off-switch на общем инстансе.
- **Never-raise:** каждый раздел — свой `try/except` + `logger.warning` + дефолт (`null`/`[]`/`{}`);
`build_metrics()` никогда не пробрасывает. Read-only: ни одного `INSERT/UPDATE/DELETE/CREATE/ALTER`.
## Альтернативы
- **Расширить `/queue`** — отклонено: ломает байт-в-байт контракт (BR-6) + смешивает сырьё с
человеческим снимком.
- **Prometheus/OpenMetrics** — отклонено: заказчик задал тонкий кастомный sidecar (не Prometheus),
контракт — JSON.
- **Орк считает CPU-дельту сам** — отклонено: требует состояния; stateful-арбитр это sidecar (C-1).
- **Расширить SELECT `get_running_jobs()`** — отклонено: перенос инварианта hot-path reaper'а;
изолируем dedicated helper.
- **Push в sidecar** — отклонено: нарушает разделение C-1; зависший орк ⇒ pull падает = сам сигнал.
## Последствия
- F1b разблокирована стабильным машинным контрактом; домен наблюдаемости стартует.
- Строго read-only + never-raise ⇒ near-zero риск для общего прод-конвейера (enduro-trails);
`/health`/`/status`/`/queue` байт-в-байт; гейты/схема/machine-verdict-ключи не тронуты (NFR-5).
- `schema_version` + аддитивно-толерантная политика ⇒ расширения не ломают F1b.
- Плата: новая поверхность совместимости `/metrics`↔F1b (митигейшн — единый репо контракта + версия);
CPU-liveness Linux-специфичен (`/proc`; не-Linux → `null`). Топология/схема не меняются (sidecar и
его сетевая достижимость — объём F1b).
- Новый компонент + публичный контракт → `arch:major-change` (хоть и аддитивно/read-only/обратимо);
прод-деплой строго через staging-гейт (8501), без рестарта прод-контейнера.
- **Откат:** `metrics_endpoint_enabled=False` (мгновенный) или удаление модуля/эндпоинта/helper'ов —
без следов в БД/схеме.
## Связи
adr-0002 (job-queue/circuit-breaker — источник `queue`-сырья), adr-0011 (job-reaper —
`get_running_jobs`/pid/liveness-семантика, изоляция hot-path), adr-0026 (терминал `{done,cancelled}`
— фильтр `stages`), adr-0017 (serial_gate — паттерн leaf `snapshot()`/never-raise), adr-0020
(frontmatter-контракт — стиль версионируемого контракта). Прямой потребитель — **F1b** (sidecar
`watchdog/`, отдельная задача).

View File

@@ -1,92 +0,0 @@
---
work_item: ORCH-057
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0031: Нормализация legacy root-owned файлов при миграции uid — детект-leaf + actionable worktree-ошибка
- **Статус:** proposed
- **Дата:** 2026-06-10
- **Задача:** ORCH-057 (follow-up ORCH-040)
- **Детальный ADR:** `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md`
## Контекст
ORCH-040 перевёл контейнеры на `user: "1000:1000"`, изменив только `docker-compose.yml`. Владельца
уже существующих `root:root` файлов в bind-mount `/repos` это не меняет. Под uid 1000
`src/git_worktree.py::ensure_worktree` (`os.makedirs` стр. 78 / `git worktree add` стр. 81/85) не может
создать worktree рядом с root-owned `/repos/_wt/``fatal: could not create leading directories …
Permission denied`, который сейчас пробрасывается сырым. Конвейер приходит сюда из
`launcher._spawn`/`_materialize_deferred_branch` (ORCH-088) — **агент не стартует** (launch-time
инфра-сбой, не код задачи). Инцидент 06.06 на проде (первый запуск ORCH-043); workaround Стрима
(`chown -R 1000:1000`) наложен вручную. ADR-040 описал нормализацию абстрактно («вне объёма кода») и
не дал процедуры → баг воспроизводим на чистой среде / новом репо / после исторического запуска под
root. Контейнер бежит **без root** → код физически не может `chown` чужие файлы; ему доступны лишь
детект + диагностика.
## Решение
Три аддитивных, обратимых kill-switch'ем слоя — паттерн условного leaf-гейта (`coverage_gate`/
`serial_gate`) + best-effort startup-хук (`main.lifespan`, как lease-reclaim). `STAGE_TRANSITIONS` /
`QG_CHECKS` / `check_*` / machine-verdict-ключи (`verdict:`/`result:`/`deploy_status:`/
`staging_status:`/`security_status:`/`coverage_status:`) / схема БД — **байт-в-байт прежние**.
- **Actionable worktree-ошибка (D1):** `ensure_worktree` классифицирует класс «нет прав» (маркеры
`Permission denied`/`could not create leading directories`/`insufficient permission`/`EACCES`/
`EPERM`) и поднимает `RuntimeError` с причиной (legacy root-файлы после миграции uid) + лечащей
командой + ссылкой на INFRA.md. Не-прав-ошибки сохраняют прежний текст/смысл (никакой подмены).
Меняется лишь **формулировка**, не факт сбоя.
- **Детект-leaf `src/fs_normalize.py` (D2):** чистый, never-raise, TTL-кэш (паттерн `preflight`).
`scan_ownership(roots, target_uid)` обходит `/repos/_wt`, `<repo>/.git/objects`,
`<repo>/.git/worktrees`, `data/runs`; ранний выход при первом `st_uid != target_uid`
(`target_uid=os.getuid()` по умолчанию). `applies(repo)` (kill-switch + scope; пусто →
`is_self_hosting_repo`) проверяется ПЕРВЫМ → дорогой обход только при applies. Идемпотентно;
ошибка обхода → WARNING + консервативный `mismatch=False`.
- **Интеграция = наблюдаемость, без блокировки claim (D3):** best-effort `scan_ownership()` на старте
`main.lifespan` → WARNING + Telegram при mismatch. Claim НЕ гейтится: внятный ранний отказ даёт D1
в точке launch (знает repo, агент ещё не тратил токены). Блокирующий preflight-гейт отвергнут —
preflight не знает repo, заблокировал бы и enduro-trails на общем `/repos`.
- **Опц. `normalize()` (D4):** chown только при `CAP_CHOWN`/root (под uid 1000 — no-op + лог),
флаг `fs_normalize_auto` (дефолт `False`). Init-контейнер/root-entrypoint отвергнут: реинтродукция
root-контекста (анти-цель ORCH-040) + правка compose = self-deploy/групповой риск. Реальную
нормализацию несёт операторская процедура.
- **Процедура (D5):** `INFRA.md` получает раздел «Миграция uid: обязательная нормализация legacy
root-файлов» (точные команды по всем корням) как обязательный шаг миграции; forward-breadcrumb из
ADR-040.
- **Флаги:** `fs_normalize_enabled` (kill-switch, дефолт `True`), `fs_normalize_repos` (CSV, пусто →
self-hosting only), `fs_target_uid` (1000), `fs_normalize_auto` (`False`), `fs_scan_roots`,
`fs_scan_cache_ttl_s` (300). Наблюдаемость — блок `fs_ownership` в `GET /queue`; опц. `POST
/fs-normalize/check`.
## Альтернативы
- **Init-контейнер/root-entrypoint** — реинтродукция root (анти-цель ORCH-040), self-deploy compose,
групповой риск ради разовой операции. Отвергнуто; носитель нормализации — операторская процедура.
- **Блокирующий claim-гейт (preflight)** — preflight не знает repo → регресс enduro на общем `/repos`.
Отвергнуто.
- **Блокирующий claim-гейт (queue_worker/claim)** — дорогой FS-обход в hot-path + «молчаливое
зависание» вместо диагноза D1. Отвергнуто.
- **Авто-chown из app по умолчанию** — под uid 1000 невозможен; ложное ожидание самолечения.
Отвергнуто (оставлен opt-in `fs_normalize_auto`).
- **Hard-fail старта при mismatch** — нарушает never-raise, стопорит сервис всех проектов. Отвергнуто.
## Последствия
- Класс «сырой git-fatal на launch после миграции uid» закрыт внятным диагнозом (D1) + проактивным
startup-сигналом (D3); пробел процедуры ADR-040 закрыт (INFRA.md).
- Нулевая регрессия enduro-trails (scope first); инварианты конвейера/схема БД — байт-в-байт.
- Никакого root-контекста/рестарта прода/касания `main`/force-push/прод-образа (NFR-1).
- Плата: фактический `chown` остаётся ручным операторским шагом (но теперь внятным, с инструкцией);
+1 best-effort startup-хук и leaf-модуль; `fs_normalize_auto=True` под root реинтродуцирует
chown-контекст (дефолт `False`, не для прод-self).
- Аддитивно/обратимо: **не** `arch:major-change` (нет новой стадии/QG/таблицы/смены топологии) — leaf
+ startup-хук + docs.
- **Откат:** `fs_normalize_enabled=False` → полный no-op (мгновенный обратимый kill-switch).
## Связи
adr-0005 (контейнер под host-uid — порождающее решение ORCH-040, чей пробел закрываем),
adr-0029/adr-0012 (coverage/security-гейт — паттерн условного leaf `applies`/scope/never-raise/
fail-open), adr-0017 (serial-gate — leaf never-raise + отложенный срез ветки `_materialize_deferred_
branch`, чья точка падает в `ensure_worktree`), adr-0011 (job-reaper — образец best-effort
startup-хука в `lifespan`), adr-0024 (disk-watchdog — образец «только читать/уведомлять, не трогать
хост/прод»).

View File

@@ -1,95 +0,0 @@
---
work_item: ORCH-019
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0032: Багфикс-трек — укороченный маршрут конвейера для багов (ORCH-019)
## Статус
Proposed
## Контекст
Любая задача идёт по полному конвейеру `analysis → architecture → development → review → testing
→ deploy-staging → deploy → done`. Для мелкого бага стадия `architecture` (отдельный прогон
opus-агента `architect` + ADR + exit-гейт `check_architecture_done`) избыточна и тратит
токены/время (прецедент ET-9/ET-014 ~35 мин).
**Корневой инвариант (нерушимый):** упрощаем только *аналитику/архитектуру*; ни один Quality
Gate / под-гейт (security/merge/coverage/image-freshness) / exit-код deploy-хука — НЕ ослаблен
(урок ET-8: срезанная проверка = недоделка на проде).
Кросс-каттинговость: затрагивает семантику маршрутизации (`advance_stage`), вводит новый
leaf-компонент `src/bug_fast_track.py` и аддитивную колонку `tasks.track` → регистрируется
сквозным ADR.
## Решение
Багфикс-трек — **свойство планировщика/точки входа, НЕ Quality Gate**.
1. **Классификация** (`src/bug_fast_track.py`, leaf never-raise по образцу `serial_gate`/`labels`):
задача с меткой Plane `Bug` (`bug_fast_track_label`, читается аппаратом ORCH-089
`labels.has_label`) помечается `track='bug'`. `applies(repo)` (локально, без сети) — первым;
`has_label` (сеть) — только при `applies==True`; чтение метки **только** в `start_pipeline`,
никогда в горячем `claim_next_job` (anti-stall).
2. **Хранение** — аддитивная идемпотентная колонка `tasks.track TEXT DEFAULT 'full'`
(`_ensure_column`, паттерн `tasks.cancelled_at` ORCH-090); читается в `advance_stage` из БД
(не из сети).
3. **Routing-override**`STAGE_TRANSITIONS` и `get_next_stage`/`get_agent_for_stage` остаются
**чистыми** (1:1). В `advance_stage`, на ребре выхода из `analysis`, при `track='bug'`:
`next_stage``development` (вместо `architecture`), `next_agent``developer` (вместо
`architect`). Багфикс физически минует стадию `architecture` → её exit-гейт
`check_architecture_done` и `06-adr/` для багфикса не исполняются.
4. **Гейт `analysis` не трогаем**`check_analysis_complete`/`check_analysis_approved` байт-в-байт
прежние; lite-аналитик эмитит все 4 файла (01-bug-report / 02-03 краткие заглушки / 04 план
обязательного регресс-теста). Экономия — пропуск всей стадии `architecture`, не число файлов.
5. **Эскалация** (обратимость) — `POST /bug-fast-track/escalate?work_item=<id>` сбрасывает
`track→'full'` (+ self-escalate мини-аналитика); задача далее идёт через `architecture`.
6. **Условность/откат**`bug_fast_track_enabled` (kill-switch), `bug_fast_track_label`,
`bug_fast_track_repos` (CSV; **пусто → self-hosting only**). `False`/неприменимый репо →
путь старта и маршрут **байт-в-байт** прежние.
7. **Наблюдаемость** — read-only блок `bug_fast_track` в `GET /queue` (флаг/область/метка +
счётчик `track='bug'` + метрика экономии из `agent_runs`); лог на решение о маршруте; опц.
`🐞` в Telegram-карточке.
## Кросс-каттинговые инварианты (НЕ нарушаются)
- `STAGE_TRANSITIONS` структурно не меняется (нет новых/удалённых стадий); `cancelled`/`done`
стоки и предикаты терминальности (ORCH-090) не затронуты.
- Реестр `QG_CHECKS`, сигнатуры `check_*`, вердикт-ключи (`verdict:`/`result:`/`deploy_status:`/
`staging_status:`/`security_status:`/`coverage_status:`), порядок под-гейтов — байт-в-байт.
- Врезка ORCH-019 в `advance_stage` — ТОЛЬКО на ребре выхода из `analysis`, ДО всех deploy-edge
под-гейтов (ORCH-022/043/027/058) и Phase A/B (ORCH-036/059) → их инварианты сохранены.
- Композиция с serial-gate (ORCH-088), auto-label (ORCH-089), coverage-gate (ORCH-027),
merge-gate (ORCH-043) — багфикс-задача остаётся обычной задачей репо.
## Последствия
- **+** Багфикс минует стадию `architecture` (основная экономия), гейты качества сохранены.
- **+** Аддитивно, под kill-switch, per-repo, never-raise, fail-safe → полный цикл; нулевая
регрессия для enduro и orchestrator при выключении.
- **** lite-аналитик эмитит 02/03 заглушки (компромисс ради неизменности гейта); эскалация v1
требует операторского действия (авто-триаж сложности — будущее, ORCH-13/Вариант 3).
- **Откат:** `bug_fast_track_enabled=False` (мгновенно); колонка `tasks.track` аддитивна и
безвредна (дефолт `'full'`).
## Связанные решения
- ORCH-089 (auto-label) — переиспользуемый аппарат label-чтения: [adr-0018](adr-0018-auto-label-gates.md)
- ORCH-088 (serial gate) — композиция очереди репо
- ORCH-027 (coverage-gate) — структурный союзник BR-4: [adr-0029](adr-0029-coverage-gate.md)
- ORCH-090 (cancelled) — паттерн аддитивной колонки `tasks.*`: [adr-0026](adr-0026-stop-cancel-task.md)
## Ссылки
- Детальный ADR задачи: `docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md`
- BRD/TRZ/AC: `docs/work-items/ORCH-019/01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`
</content>

View File

@@ -1,85 +0,0 @@
---
work_item: ORCH-100
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0033: Sidecar-watchdog F1b — мозг мониторинга в отдельном контейнере
- **Статус:** proposed
- **Дата:** 2026-06-10
- **Задача:** ORCH-100 (FND/F1b)
- **Детальный ADR:** `docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`
- **Парный ADR:** `adr-0030` (F1a `/metrics` — источник сырья)
## Контекст
Домен 0 «Фундамент» эпика автономного саморазвития, рамка наблюдаемости заказчика: **наблюдатель
отделён от наблюдаемого**. F1a (adr-0030) отдаёт read-only `GET /metrics`**только сырьё**. F1b —
**мозг**: читает сырьё, дополняет внешними сигналами (хост/контейнеры/зависимости), решает по порогам,
алертит. Частичные стражи (`disk_watchdog`/`reaper`/`reconciler`) живут ВНУТРИ процесса орка — орк
завис/упал ⇒ они мертвы, платформа слепа в критический момент. Рамки: C-1 (отдельный контейнер, код в
`watchdog/`), C-2 (без внешнего плеча — принятый риск), C-3 (тонкий стек, НЕ Grafana/Prometheus; хост
впритык). Критический инвариант: орк лёг ⇒ `/metrics` недоступен = **сам сигнал тревоги**.
## Решение
Новая папка `watchdog/`**тонкий Python-3.12-stdlib демон** (без сторонних зависимостей), отдельный
образ `watchdog/Dockerfile` + сервис `orchestrator-watchdog` в `docker-compose.yml` (`network_mode:
host`, read-only `docker.sock`, `mem_limit: 128m`, `restart: unless-stopped`). Тик: (1) `GET /metrics`;
(2) хост (диск/inode/память/CPU, stdlib); (3) статусы контейнеров через read-only `docker.sock`
(GET-only — без `docker` SDK); (4) пинг Plane/Gitea/Anthropic. Сигналы проходят через **обобщённую
чистую** `decide(signal_active, prev, now, cooldown) -> alert|realert|recovery|none` (генерализация
`disk_watchdog.decide_action`; per-signal in-memory `AlertState`). Алерт — в **собственный** Telegram-
канал sidecar (свои `WATCHDOG_TG_*`; **НЕ** импорт `src/notifications.py`). Особый сигнал — `/metrics`
не отвечает → `orch_down`. Всё never-raise (per-source/per-tick/per-send), под kill-switch
`WATCHDOG_ENABLED`, строго read-only к наблюдаемому. **`src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/
`check_*`/схема БД орка — не тронуты** (F1b вне процесса орка и вне конвейера QG).
- **Стек** — Python stdlib (`urllib`, `socket`+`http.client` для docker.sock, `shutil.disk_usage`,
`/proc/meminfo`); pytest на чистые функции. Отвергнуты Go / `docker` SDK / Prometheus (C-3).
- **Реестр сигналов** — `orch_down` (K подряд неудачных опросов), `host_mem`/`host_disk_crit`,
`agent_hung``cpu_ticks`/`clk_tck``generated_at` < floor при растущем `runtime_s`; нужно 2
опроса — sidecar stateful-арбитр), `stage_stuck` (`age_in_stage_s`), `job_failed` (edge),
`queue_depth`, `container_down` (per name), `dep_down` (per name). Пороги/интервалы/URL — из env.
- **Владелец диск-алерта (BR-10)** — штатные 85% остаются за внутренним `disk_watchdog` (ORCH-063,
канал орка) ⇒ **нулевой дубль по построению**; sidecar покрывает провал «орк+disk_watchdog мертвы»
через `orch_down`, плюс **opt-in** (default off) независимый критический потолок `host_disk_crit`
(97%) — другое событие/канал, не повтор 85%.
- **Толерантность контракта** — неизвестные ключи `/metrics` игнорируются, отсутствие опционального не
ошибка, рост `schema_version` → warning (зеркало аддитивной политики adr-0030).
- **Kill-switch** `WATCHDOG_ENABLED=false` → демон инертен (idle-loop, не exit) ⇒ нулевой эффект.
## Альтернативы
- **Go / `docker` SDK / `requests`** — отклонено: вес/вторая цепочка против C-3 и консистентности с
`disk_watchdog`.
- **Prometheus/Grafana/TSDB** — отклонено: прямой запрет C-3.
- **Sidecar — единственный владелец диска** — отклонено: потеря покрытия, когда сам sidecar/Docker
недоступен; выбрана связка primary `disk_watchdog` + opt-in ceiling.
- **Push из орка в sidecar** — отклонено: зависший орк не пушит; pull падает = сам сигнал `orch_down`.
- **bridge + `host.docker.internal`** — отклонено: на Linux ненадёжно; `network_mode: host` проще.
- **Своя БД/файл порогов** — отклонено: C-3; in-memory best-effort достаточно (как `disk_watchdog`).
## Последствия
- Внешний мозг мониторинга переживает падение орка; `orch_down` делает наблюдателя громче в инцидент.
- Строго read-only + независимый канал + never-raise ⇒ self-hosting-безопасно (enduro не затронут);
падение sidecar не влияет на конвейер.
- Аддитивно/обратимо: `src/**`/гейты/схема байт-в-байт; kill-switch → нулевая регрессия; дубль диска
исключён структурно.
- Плата: новый контейнер на впритык-хосте (`mem_limit: 128m` + замер RSS на staging обязательны);
C-2 (падёт хост → молчит и sidecar); новая поверхность совместимости `/metrics`↔F1b (толерантный
парсинг + единый репо контракта); CPU-liveness Linux-специфичен.
- **Топология** меняется (новый контейнер) → `07-infra-requirements.md`; **схема БД** не меняется →
08 = N/A. Новый компонент + контейнер + канал → `arch:major-change`; прод-выкат через staging-гейт
(8501), деплой sidecar НЕ рестартит прод-контейнер.
- **Откат:** не запускать сервис / `WATCHDOG_ENABLED=false` (мгновенный) или удаление `watchdog/` +
сервиса + env — без следов в БД/схеме.
## Связи
adr-0030 (F1a `/metrics` — парный источник сырья; контракт `cpu_ticks`/`clk_tck`/`generated_at`/
`schema_version`), adr-0024 (`disk_watchdog` — образец решающей функции/never-raise + владелец
диск-алерта), adr-0025 (build-cache-pruner — паттерн «вторая половина»), adr-0017 (serial_gate —
leaf `snapshot()`/never-raise), adr-0011 (job-reaper — pid/liveness-семантика). Прямой источник —
**F1a** (`GET /metrics`); F1b — его потребитель.
</content>

View File

@@ -1,92 +0,0 @@
---
work_item: ORCH-098
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0034: Машинный журнал уроков — таблица `lessons` + observer-leaf (ORCH-098)
## Статус
Proposed
## Контекст
Оркестратор автономно ведёт задачи по конвейеру (ORCH-54), но **развивается** вручную: инциденты →
уроки → задачи. Уроки живут свободным текстом в `memory/` — не машиночитаемы: нельзя считать
паттерны, приоритизировать, предлагать улучшения. ORCH-098 — шаг 1 эпика саморазвития (домен 0
«Фундамент», F2): «топливо» петли самообучения 8A. Нужна **структурированная таблица отклонений
конвейера**, на которой позже встанут ретроспективщик (E2), приоритизатор RICE (E3) и Стрим.
Нормативное требование Славы (10.06): схема ДОЛЖНА **сразу** нести поля **атрибуции** урока
(`platform`/`project`/`both`/`unknown` + целевой репо + домен улучшения), иначе позже придётся
переделывать схему на живой общей прод-БД.
**Кросс-каттинговость** (почему сквозной ADR): новый компонент `src/lessons.py` + аддитивная
таблица на **общей прод-БД** (self-hosting, разделяемой с enduro-trails) + врезки автозаписи в
несколько горячих choke-point'ов (`stage_engine`/`merge_gate`/`launcher`) + новый раздел контракта
`GET /queue`. Фундамент для будущих задач-потребителей → регистрируется глобально.
## Решение
Журнал уроков — **observer (наблюдатель), НЕ Quality Gate**. Аддитивная таблица + чистый leaf,
по образцу `serial_gate`/`coverage_gate`/`metrics`/`bug_fast_track`.
1. **Таблица `lessons`** (`db.init_db()`, `CREATE TABLE IF NOT EXISTS` + 3 индекса, идемпотентно,
restart-safe) — поля контекста (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализа
(`root_cause`/`suggestion`), статуса (`status`/`related_task`), **атрибуции сразу и нуллабельно**
(`attribution`/`target_repo`/`target_domain`) + `source`/`detail`. Без `enum`-констрейнтов
(слаги forward-compatible). Будущие колонки — `_ensure_column`.
2. **Leaf `src/lessons.py`** (never-raise, импортирует только `config`+`db`): `record()` / `get()` /
`update()` / `snapshot()`. **Расхождение с гейт-шаблоном: журнал НЕ скоупится по репо** — он
observer-only и не *действует* ни на один репо; единственный регулятор — глобальный kill-switch
`lessons_enabled`. Запись урока про enduro ценна и **не затрагивает** пайплайн enduro (чистая
память орка); репо-разрез — на выборке (`repo`-колонка/фильтр).
3. **Автозапись 4 типов** (`source="auto"`, best-effort, дедуп в окне; `transient_retry` — только на
исчерпании бюджета ретраев): `gate_failure` (`stage_engine._handle_qg_failure_rollbacks`),
`merge_hold` (`merge_gate._handle_merge_verify` HOLD), `transient_retry` (merge-retry/launcher
transient budget-exhaustion), `deploy_degraded` (post-deploy `DEGRADED → set_repo_freeze`, урок
слоя-3 «деплой OK / прод сломан», ET-8). Каждая врезка — одиночный вызов в защитном `try/except`.
4. **Эндпоинты** `GET /lessons` (read-only, фильтры), `POST /lessons` (ручная запись,
`source="manual"`), `POST /lessons/{id}` (update — доклассификация `unknown`), + read-only ключ
`"lessons": snapshot()` в `GET /queue`. При выключенном флаге → `{"enabled": false}`.
**Инвариант (нерушимый):** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи
(`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/`coverage_status:`) /
схемы существующих таблиц — **байт-в-байт не тронуты**. Журнал не влияет на продвижение по стадиям.
## Композиция с существующими механизмами
- **Self-hosting (общая БД):** аддитивная таблица; enduro не затронут (NFR-3).
- **serial-gate (ORCH-088) / post-deploy (ORCH-021):** детектор `deploy_degraded` врезан рядом с
`set_repo_freeze`, не меняя freeze-логику.
- **merge-gate (ORCH-043/071/093):** `merge_hold`/`transient_retry` читают исход актора, не меняя
классификатор/ретрай.
- **metrics (ORCH-099):** журнал — историческая память петли (best-effort запись), `/metrics`
realtime-сырьё для sidecar; разные роли, оба observer-only.
## Условность и откат
- Флаг `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`; kill-switch) +
`lessons_dedup_window_s` / `lessons_query_limit_default`. `False` → полная инертность, нулевая
регрессия, конвейер байт-в-байт прежний.
- **never-raise** на всех публичных функциях и врезках (NFR-1) — сбой журнала не роняет конвейер.
- Откат — флаг в `false` (мгновенно) или revert диффа; таблица не касается существующих.
## Последствия
- **+** Машиночитаемые уроки — фундамент E2/E3/Стрим; атрибуция forward-proof (без передела живой БД).
- **+** Нулевая регрессия; проверенный additive-observer-leaf шаблон → низкий риск; enduro изолирован.
- **** Рост таблицы (митигейшн: лёгкие строки + дедуп + budget-exhaustion; ретенция — будущее).
- **** Дедуп-запрос в `record()` (один indexed-SELECT, только `auto`).
## Ссылки
- Локальный ADR: `docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md`
- BRD/TRZ/AC: `docs/work-items/ORCH-098/01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`
- Data/Infra/Risks: `docs/work-items/ORCH-098/08-data-requirements.md`, `07-infra-requirements.md`,
`10-tech-risks.md`
- Эпик: `docs/epics/self-evolution.md` (домен 0 «Фундамент», F2; петля 8A)
- Сверено по коду: `src/serial_gate.py`, `src/coverage_gate.py`, `src/db.py`, `src/stage_engine.py`,
`src/merge_gate.py`, `src/agents/launcher.py`, `src/main.py`, `src/qg/checks.py`.

View File

@@ -1,80 +0,0 @@
---
work_item: ORCH-009
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-10
model_used: claude-opus-4-8
---
# adr-0035: Turnkey-онбординг проектов — kit + операторский CLI + runbook (ORCH-009)
## Статус
Proposed
## Контекст
Подключение нового проекта к оркестратору — ручная археология по разрозненным докам и памяти;
каждый пропущенный шаг даёт **тихую деградацию**: без промптов в репо конвейер проекта не работает
вовсе (launcher резолвит `.openclaw/agents/<role>.md` относительно worktree репо задачи); без
точных имён статусов Plane ветки `Confirm Deploy` (ORCH-059) / `STOP` (ORCH-090) молча не
активируются (fail-closed); без лейблов `autoApprove`/`autoDeploy`/`Bug` авто-режимы (ORCH-089)
и багфикс-трек (ORCH-019) молча выключены (fail-safe). Эталон онбординга — **сам репозиторий
orchestrator** (каноны ORCH-52b/c/d/e кодифицированы в `docs/_templates/`, `docs/_standards/`,
`.openclaw/agents/`). Домен D5.2 эпика саморазвития: способность разворачивать новый проект
одним проходом.
## Решение
Способность реализуется **вне рантайма и вне конвейера**`src/**` байт-в-байт не меняется
(`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД/контракт `projects.py`
нетронуты), kill-switch не нужен (активация — только явный запуск операторского CLI):
1. **Onboarding-kit `onboarding/repo-skeleton/`** — параметризуемый каркас нового репо:
6 промптов агентов канона 52d/92 (5 XML-секций, «❌→✅», эмиссия схемы 52c, verdict-ключи
байт-в-байт; язык — канон орка: 5 ru + deployer en), паспорт `CLAUDE.md`, `AGENTS.md`
(точка входа агентов), `CONTRIBUTING.md`, `README.md`, `CHANGELOG.md`, скелет `docs/` с
обязательным `operations/INFRA.md`, `.env.example`. Плейсхолдеры `{{NAME}}` + stdlib-рендер
(без новых pip-зависимостей); словарь — `onboarding/placeholders.json` (биекция со
вхождениями в kit держится тестами). **Канон не форкается:** `docs/_templates/` +
`docs/_standards/` НЕ хранятся в kit — копируются live из чекаута орка в момент материализации.
2. **Операторский CLI `scripts/onboard_project.py`**`plan` (дефолт, GET-only, ни одной
мутации) / `apply` (идемпотентный ensure, без delete-операций) / `verify`. Шаги: Plane-проект →
22 статуса с точными именами из `plane_sync._PLANE_NAME_TO_KEY` (read-only импорт — нулевой
дрейф; канонические группы фиксированы: `STOP``cancelled`, терминальные группы только у
Done/Cancelled/STOP — иначе terminal-detection ORCH-068 ложно терминалит) → лейблы → Gitea-репо
(+per-repo webhook `push`/`pull_request`/`status`; HMAC-секрет **переиспользуется** из
`ORCH_GITEA_WEBHOOK_SECRET` — приёмник один на все репо) → материализация kit + initial push
**только в свежесозданный пустой репо** (INV-4 не затрагивается) → merged-вывод
`ORCH_PROJECTS_JSON`, провалидированный фактическим `projects._parse_projects_json`
(round-trip). Недоступное в Plane CE API → `manual-step` со ссылкой на runbook (fail-safe).
Скрипт **никогда** не рестартит прод, не правит `.env`, не пушит в существующие репо, ничего
не удаляет.
3. **Runbook `docs/operations/ONBOARDING.md`** — полный чеклист: предусловия (токены) → скрипт →
операторские шаги (env + управляемый рестарт с self-hosting-предупреждением; UI-only Plane) →
верификация (`verify` + smoke) → откат. Smoke-контур — **staging (8501, изолированная БД)** +
одноразовый sandbox-проект (`SMK`); протокол — «Журнал smoke-прогонов» в runbook.
Анти-дрейф — структурные тесты kit (аналог `tests/test_agent_prompts_canon.py`) + снапшот-тест
`STAGE_TRANSITIONS`/`QG_CHECKS` (контроль ненарушения `src`). Branch protection `main` новых репо
**не включается** (ломала бы PR-merge API merge-актора — ложные HOLD класса ORCH-093).
## Последствия
- **+** Новый проект разворачивается одним проходом проверяемо: все слои (Plane-контракты,
webhook, промпты, дока, реестр) закрыты скриптом+runbook; тихие деградации ловит `verify`.
- **+** Нулевой риск рантайма: изменение docs/templates/scripts/tests-only; регресс
enduro/orchestrator невозможен по построению; общая БД не читается и не пишется скриптом.
- **+** Единый эталон без форка: новые репо получают живой канон момента онбординга;
обновления канона в них едут обычными PR с reviewer-gate.
- **** Регистрация в реестре остаётся операторской (env + управляемый рестарт — Ф-3,
сознательное ограничение NFR-2); разрыв «создано, но не зарегистрировано» виден через `verify`.
- **** Закрытый список read-only импортов из `src` (`projects._parse_projects_json`,
`plane_sync._PLANE_NAME_TO_KEY`, поля `config.settings`) — связь с приватными именами;
поломка при рефакторинге видимая (тесты), расширение списка — только через ADR.
- **Ограничение:** способность ≠ исполнение: онбординг конкретного заказчика — операторская
эксплуатация (вне ORCH-009); тиражирование на новый хост — ORCH-10 (вне объёма).
Детально: `docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md`
(D1…D11 — раскладка, плейсхолдеры, copy-vs-template split, импорт src, группы статусов,
webhook-секрет, формат реестра, smoke-контур, языковая политика, branch protection, форма CLI).

View File

@@ -1,316 +0,0 @@
# 🧬 ЭПИК: Автономное саморазвитие платформы оркестратора
> **Статус:** концепция v2 (структура согласована Славой 09.06 → ждёт финального апрува → декомпозиция)
> **Автор:** Стрим · **Дата:** 2026-06-09 · **Заказчик:** Слава
> **Связанные:** ORCH-8 (петля самообучения), ORCH-83 (наблюдаемость), ORCH-54 (автономное внедрение, done)
> **Источники:** память орка (инциденты 0609.06), инвентаризация 94 задач Plane, мировые практики (STRATUS NeurIPS'25, ChaosEater ASE'25, self-healing LLM-agents arXiv'26, agentic AIOps, FinOps token-economics).
---
## 0. Зачем это (vision)
Оркестратор уже **автономно внедряет** (ORCH-54: задача проходит analysis→prod без человека). Но автономность исполнения ≠ автономное **развитие**. Сегодня платформу развивает связка Слава+Стрим вручную: ловим инциденты → формулируем уроки → заводим задачи → апрувим.
**Цель эпика:** управляемый самоподдерживающийся контур, где платформа сама замечает свои слабые места И возможности роста, предлагает улучшения как готовые задачи, проводит их через собственный конвейер (ORCH-7 self-hosting) — **под контролем человека на ключевых развилках** (safety > автономность).
**Принцип баланса (коррекция Славы 09.06):** саморазвитие — это НЕ только «не падать и не косячить». Стабильная платформа, которая не растёт в возможностях, — тупик. **Рост функционала (новые фичи, стеки, удобства для заказчиков) — равноценный домен, а не следствие надёжности.** Платформа развивается по двум рукам одновременно: крепнет (надёжность/качество/экономика) И раздаётся вширь (возможности/масштаб).
---
## 1. Архитектура эпика: фундамент + 5 доменов + 2 вертикали
```
┌─────────────────────────────────────────────────────────────┐
│ ВЕРТИКАЛЬ-ДВИГАТЕЛЬ 🧠 ВЕРТИКАЛЬ-ТОРМОЗ 🛑 │
│ 🔄 уроки (крепнем) + governance / safety L0-L3 │
│ 💡 генератор идей (растём) (ограничивает, апрувы) │
│ ░░░░░░░░░░░░ проходят СКВОЗЬ все домены ░░░░░░░░░░░░░░░░░░░░░ │
├─────────────────────────────────────────────────────────────┤
│ ДОМЕНЫ РАЗВИТИЯ (равноценные, две руки роста) │
│ │
│ КРЕПНЕТ ───────────────────► РАЗДАЁТСЯ ВШИРЬ ────────► │
│ 🛡️ D1 Надёжность 🚀 D4 Возможности (фичи) │
│ ✅ D2 Качество/Доверие 📈 D5 Масштаб │
│ 💰 D3 Экономика │
├─────────────────────────────────────────────────────────────┤
│ ФУНДАМЕНТ (слой 0): 👁️ Наблюдаемость + 📒 Журнал уроков │
│ глаза и память — без них всё слепо │
└─────────────────────────────────────────────────────────────┘
Общая метрика-объединитель: 🌡️ ГРАДУСНИК АВТОНОМНОСТИ
(каждый домен двигает её вверх контролируемо)
```
### Что изменилось против v1 (мои же правки по критике)
- **Наблюдаемость вынесена в фундамент** (была внутри M1) — она питает ВСЁ.
- **M0 разбит на 2 вертикали:** двигатель (петля) и тормоз (governance) — у них противоположная логика, нельзя в одну коробку.
- **Добавлен домен D2 Качество/Доверие** — была дыра: надёжная платформа может стабильно генерить говнокод. Надёжность инфры ≠ корректность результата.
- **Рост (D4+D5) — равноценные домены, не «второй эшелон»** (коррекция Славы).
- **Градусник автономности** — сквозная измеримая цель вместо абстракции.
---
## 🏗️ АРХИТЕКТУРНЫЕ РАМКИ наблюдаемости (решено Славой 09.06 — constraints для архитектора)
> Это НЕЗЫБЛЕМЫЕ границы (заказчик). Конкретные ADR (стек, формат метрик, точки врезки) — зона архитектора внутри этих рамок.
**Принцип:** наблюдатель ОТДЕЛЁН от наблюдаемого. Мониторинг НЕ живёт внутри орка — иначе орк упал/завис/съел память → мониторинг ляжет вместе с ним, и мы слепы в самый критичный момент.
**Решения Славы:**
- **С-1. Sidecar-контейнер на том же хосте** (вариант A). Отдельный процесс/память/рестарт — орк падает, наблюдатель жив и РЕПОРТИТ это.
- **С-1б. КОД sidecar — В РЕПО орка** (отдельная папка `watchdog/`), рантайм — ОТДЕЛЬНЫЙ контейнер. Изоляция — на уровне КОНТЕЙНЕРА, не репозитория. Плюсы: (1) конвейер орка пилит свой мониторинг сам (self-hosting ORCH-7); (2) контракт `/metrics`↔sidecar в одном репо — не разъедется (один PR/тесты); (3) один CI. Сборка: ОТДЕЛЬНЫЙ `watchdog/Dockerfile` + сервис `orchestrator-watchdog` в docker-compose.yml. Разовое инфра-действие: добавить сервис в compose + первый запуск (Слава/Стрим на хосте), дальше код watchdog катится через конвейер.
- **С-2. Без внешнего плеча (L2).** Не усложняем второй площадкой. (Принятый риск: падёнвесь хост/Docker → наблюдатель тоже молчит; осознанно.)
- **С-3. Тонкий стек.** НЕ Grafana+Prometheus (+5-6 контейнеров на забитый хост). Тонкий Python/Go sidecar. **Факт хоста 09.06: RAM 171Mi free / 7.7Gi, диск 92%** — ресурсы впритык, наблюдатель обязан быть лёгким.
**Разделение ответственности:**
- **Орк отдаёт только сырьё:** лёгкий read-only `/metrics` (свои внутренние данные — стадии/очередь/agent-liveness/cost, что знает только он). БЕЗ логики мониторинга/алертов/хранения. Орк лёг → endpoint недоступен = САМ сигнал тревоги.
- **Sidecar — мозг мониторинга:** читает `/metrics` орка + хост (диск/память/CPU) + контейнеры (docker.sock read-only) + пинг Plane/Gitea/Anthropic; хранит пороги, шлёт Telegram-алерты СО СВОИМ каналом (не зависит от кода орка).
- **Журнал уроков (F2)** — исключение: это НЕ realtime-мониторинг, а историческая память петли → допустимо в БД орка (аддитивная таблица). Не критично к падению орка в момент (запись best-effort).
---
## 2. ФУНДАМЕНТ (слой 0) — 👁️ Глаза и 📒 Память
Без данных нечем ни чинить, ни считать, ни приоритизировать, ни учиться. Строится первым.
- **F1 Наблюдаемость** (ORCH-83 [ЭПИК]): метрики agent-liveness + очередь + стадии + хост (диск/память/CPU) + контейнеры + внешние деп (Plane/Gitea/Anthropic). Эндпоинты /health /status /queue → расширить до /metrics + дашборд.
- **F2 Журнал уроков** (ORCH-8 шаг 1): машинная структурированная таблица отклонений (тип, контекст, корень, предложение, статус) — формализовать то, что сейчас в memory/. Это «топливо» для вертикали-двигателя.
### 🎯 СКОП НАБЛЮДЕНИЯ — три слоя (решено Славой 10.06)
> Граница «мониторим ПЛАТФОРМУ vs ПРОДУКТЫ на ней». Важно для архитектора и будущих задач — не путать уровни.
- **Слой 1 — проекты как ЗАДАЧИ в конвейере — ✅ В СКОПЕ (F1a/F1b).** ET-задачи в stages/queue/agents `/metrics` — это работа орка (его агенты/очередь/стадии). Sidecar алертит «ET-задача застряла». Здоровье КОНВЕЙЕРА.
- **Слой 2 — проекты как КОНТЕЙНЕРЫ на хосте — ✅ В СКОПЕ (F1b, жив/мёртв).** `enduro-trails-app-1`, `osrm` и пр. через docker.sock ro — Up/healthy/restarting/exited. Общий хост впритык → текущий ET-контейнер вредит орку. Здоровье контейнера как чёрного ящика.
- **Слой 3 — ВНУТРЕННЕЕ бизнес-здоровье продукта — ❌ НЕ В ФУНДАМЕНТЕ, НО НУЖНО (см. ниже).** Эндпоинты ET отвечают 200? карта рендерится? latency не деградировала после фичи? Орк не знает внутренностей задеплоенных приложений — это МОНИТОРИНГ ПРОДУКТА, не платформы.
**Слой 3 — это отдельная продуктовая способность (домен D4/D5):** «per-project мониторинг здоровья задеплоенного приложения» — опция для заказчика («слежу, что твой ET-сайт жив»). **НО он НУЖЕН и самой петле** (см. §8A «атрибуция уроков») — без детекции деградации продукта петле нечего ловить. Порядок: фундамент (слои 1-2) сначала, слой 3 — позже как D4/D5-фича.
---
## 3. ДОМЕН D1 — 🛡️ Надёжность (Self-Repairing)
**Есть:** reconciler (53), post-deploy monitor+rollback (21), merge-verify (71/73), reaper (65), disk-watchdog (63), build-prune (62).
**Уроки:** фантом-merge, deploy-петли, транзиенты, флапп-статусы, зомби-jobs.
- **D1.1** Предиктивный мониторинг (causal, не порог): «диск заполнится через N ч».
- **D1.2** Авто-ремедиация рантайма: каталог типовых фиксов (зомби-job→requeue, stale-lease→reclaim, флапп→форс-терминал).
- **D1.3** Транзиент-резилентность everywhere (обобщение ORCH-93): единый retry+backoff для всех внешних вызовов.
- **D1.4** Zero-downtime деплой платформы (blue-green/canary): резервное плечо вместо окна недоступности.
- **D1.5** Авто-rollback по SLO (расширение 21): откат по деградации latency/error-rate, не только health.
- **D1.6** Deep agent-liveness (self-healing LLM): «думает / завис / зациклился» по reasoning+CPU+прогрессу.
- **D1.7** Backup/restore БД+worktree (recovery после краша хоста).
---
## 4. ДОМЕН D2 — ✅ Качество / Доверие результата
> Новый домен. Закрывает дыру: платформа может надёжно и дёшево производить плохой результат. Надёжность инфры ≠ корректность кода/аналитики.
**Есть:** security-гейт (22), reviewer/tester стадии, промпт-аудит (92).
- **D2.1** Code-coverage гейт (ORCH-27): защита от деградации покрытия.
- **D2.2** Регресс-страж результата: не только «тесты зелёные», но «не сломали соседнюю фичу» (расширение regression-guard ORCH-73).
- **D2.3** Качество аналитики: метрика «BRD не пришлось переделывать», сверка факт vs ТЗ (как сегодня ловила ложное P0).
- **D2.4** Доверие к выходу: provenance артефактов, воспроизводимость, «деплой OK = прод реально работает» (урок ET-8).
- **D2.5** Опциональная человеческая приёмка важных фич (ORCH-28).
- **D2.6** Само-оценка агентов: уверенность в результате → эскалация при низкой.
---
## 5. ДОМЕН D3 — 💰 Экономика
**Боль (ORCH-38):** developer сжёг **$13.68 на мелочь** (cache_read 18.98M — слепое сканирование src/).
- **D3.1** Model-routing cascade (мир: 87%): классификатор сложности → дешёвая модель на простое, opus на сложное (ORCH-20+13).
- **D3.2** Бюджет circuit-breaker (ORCH-23): хард-лимит $/токенов/времени → пауза+алерт.
- **D3.3** Оценка задачи ДО старта (ORCH-20): прогноз $/время по истории.
- **D3.4** Целевые файлы в задании (ORCH-38): analyst даёт точный список из TRZ → нет слепого сканирования. **Самый дешёвый высокий impact.**
- **D3.5** Fast-track простых задач (ORCH-19): багфикс → урезанный цикл без architect, дешёвая модель.
- **D3.6** Semantic caching / prompt compression (мир: 31%).
- **D3.7** Cost-дашборд + детект аномалий.
---
## 6. ДОМЕН D4 — 🚀 Возможности (рост функционала)
> **Равноценный домен (акцент Славы).** Это то, ради чего платформой ПОЛЬЗУЮТСЯ. Без новых возможностей надёжность бессмысленна — нечего надёжно делать. Развивается параллельно с D1-D3, а не после.
**Backlog-зародыши:** ORCH-12/13/14/15/18/24/25.
- **D4.1** Стеки-плагины: профили стека (web/mobile/data/ML/embedded) → агенты адаптируют процесс. Расширяемо без правки ядра. **Открывает заказчикам новые типы проектов.**
- **D4.2** Android/мобильный стек (ORCH-15): полноценная разработка приложений.
- **D4.3** UX/UI-дизайнер (ORCH-14): дизайнер-агент генерит макеты на аналитике, согласование с BRD.
- **D4.4** Интерактивный аналитик (ORCH-18): живой диалог Слава↔analyst — уточнение BRD, обсуждение вариантов до старта. Удобство + качество постановки.
- **D4.5** Тяжёлые вычисления (ORCH-12): воркер/стадия для долгих расчётов (ML-обучение, миграции данных).
- **D4.6** База знаний проекта (ORCH-24): RAG-контекст решений/архитектуры — агенты умнее (+экономия).
- **D4.7** Декомпозиция эпиков (ORCH-25): эпик→задачи→сборка автоматически (этот документ — кандидат №1).
- **D4.8** Новые роли-агенты: data-engineer, ML-инженер, DevOps — по мере типов проектов.
- **D4.9** Мультипровайдерность моделей (ORCH-13): не только Claude — выбор под задачу/стек/бюджет.
---
## 7. ДОМЕН D5 — 📈 Масштаб
> Вторая «рука роста»: способность делать БОЛЬШЕ и ШИРЕ. Сейчас потолок — `max_concurrency=1`.
**Backlog-зародыши:** ORCH-9/10; done: ORCH-6 (multi-repo), ORCH-88 (serial-batch).
- **D5.1** Параллельная разработка (снять max_concurrency=1): безопасный N>1 (изоляция worktree есть, нужна merge-orchestration FIFO + защита main). **Много фич параллельно = быстрее растём.**
- **D5.2** Turnkey-онбординг проекта (ORCH-9): команда → Plane+Gitea+агенты+инфра за минуты.
- **D5.3** Тиражирование на новый хост (ORCH-10): перенос платформы на инфру нового заказчика (IaC-bundle).
- **D5.4** Горизонтальный воркер-пул: очередь jobs (ORCH-1) → несколько воркеров/хостов.
- **D5.5** Per-project лимиты ресурсов (concurrency/бюджет на проект).
- **D5.6** Мультитенантность (отложено — SaaS-сценарий, по спросу).
---
## 8. ВЕРТИКАЛЬ-ДВИГАТЕЛЬ 🧠 — две турбины: реактивная + проактивная
> Двигатель питается из ДВУХ источников (коррекция Славы 09.06). Реактивная турбина (уроки из боли) кормит «крепнем» (D1-D3). Проактивная (генератор идей) кормит «растём» (D4-D5). Без второй турбины рост фич зависит только от Славы — бутылочное горлышко.
### 8A. Реактивная турбина 🔄 — петля самообучения из уроков (ORCH-8)
```
ДЕТЕКЦИЯ → ЖУРНАЛ урока → АНАЛИЗ/паттерны → ПРЕДЛОЖЕНИЕ задачи → [governance-гейт] → конвейер ORCH-7 → проверка эффекта → журнал
```
- **Детекция:** провал гейта, **ручное вмешательство (самый ценный сигнал — каждый ручной пинок = дыра автономности)**, ретраи/откаты/таймауты, ложные срабатывания, «деплой OK / прод сломан».
- **Анализ (гибрид):** машина копит и предлагает черновик → Стрим фильтрует/оформляет → Слава апрувит.
- **E1** Журнал уроков (=F2). **E2** Агент-ретроспективщик (анализ→предложение).
#### ⚖️ АТРИБУЦИЯ урока — platform-level vs project-level (решено Славой 10.06)
> Ключевой шаг петли. Пример Славы: выпустили фичу в ET → она деградировала ET. Петля поймала сигнал — но ЧЬЯ вина и ГДЕ чинить?
Когда детектирована деградация продукта после выпуска фичи, петля ДОЛЖНА различить два уровня вины и направить урок в правильное русло:
- **А. Platform-level (недоработал ОРК):** конвейер выпустил деградацию, потому что у платформы СЛАБЫЙ ПРОЦЕСС (нет регресс-гейта «фича не ломает соседнее», тест-стадия не ловит деградацию производительности, нет производительностного бенчмарка в приёмке). → улучшаем ПРОЦЕСС орка (домен **D2 Качество** / **D1 Надёжность**). Чинится ОДИН раз — выигрывают ВСЕ проекты.
- **Б. Project-level (недоработал ПРОЕКТ):** процесс орка нормальный, но в конкретном ET МАЛО тестов/слабая приёмка под этот тип фич. → усиливаем ТЕСТЫ/приёмку В САМОМ ET (задача в бэклог ET). Чинится точечно — выигрывает только ET.
**Механизм (новый шаг петли):**
```
ДЕТЕКЦИЯ деградации продукта (слой 3) → урок →
АТРИБУЦИЯ: platform-level или project-level?
├─ platform → задача в D1/D2 (улучшить процесс — польза всем)
└─ project → задача в бэклог ET (усилить тесты ET — польза ET)
(развилка не всегда бинарна — бывает ОБА: и гейт в орк, и тесты в ET)
```
Без атрибуции петля «чинит платформу» там, где надо усилить проект (и наоборот). **Зависит от слоя-3 детекции** (§2): без мониторинга здоровья продукта петле нечего атрибутировать. **E2-ретроспективщик** несёт эту классификацию; спорные случаи → Стрим/Слава решают.
### 8B. Проактивная турбина 💡 — генератор идей новых возможностей (НОВОЕ — запрос Славы)
> Отдельный источник идей роста функционала — НЕ только требования от Славы. Проактивно предлагает новые фичи/возможности/удобства. Та же воронка: машина/агент генерит черновики → Стрим фильтрует → Слава решает.
**Источники идей (вход генератора):**
- **I1 Гэпы реализации:** чего НЕ хватило для запрошенных проектов (enduro-trails, snowbike — что было тяжело/невозможно сделать платформой → кандидат в фичу).
- **I2 Паттерны ручного труда:** что Слава/заказчики часто делают руками ВНЕ платформы → кандидат на автоматизацию/фичу.
- **I3 Тренды и новые технологии:** сканирование новых моделей/стеков/инструментов (web-поиск, release-notes провайдеров) → «вышла модель X / фреймворк Y — даёт новую возможность».
- **I4 Конкурентный/рыночный анализ:** что умеют другие AI-платформы разработки (Devin, Cursor, Copilot Workspace…) → чего нет у нас.
- **I5 Анализ собственного бэклога/истории:** паттерны типов задач → «часто просят X → стоит сделать шаблон/фичу».
- **I6 Обратная связь заказчиков:** явные пожелания/жалобы по реализованным проектам.
- **I7 Саморефлексия Стрим:** я вижу работу платформы изнутри каждый день — предлагаю удобства/фичи из опыта ведения.
**Компоненты:**
- **E4 Агент-идеатор (product-discovery):** по расписанию сканирует I1-I7 → генерит бэклог идей-черновиков фич (с обоснованием «зачем/кому/из какого источника»).
- **E5 Банк идей:** отдельный реестр (не путать с журналом уроков): идея, источник, предполагаемая ценность, статус (new/отклонена/в работе).
### 8C. Общий выход двигателя
- **E3 Приоритизатор RICE:** сводит ОБА потока (уроки из 8A + идеи из 8B) в единый ранжированный бэклог по impact/cost/risk — что брать первым по всем доменам. Баланс «крепнем vs растём» — настраиваемый (квота слотов на надёжность vs фичи).
---
## 9. ВЕРТИКАЛЬ-ТОРМОЗ 🛑 — Governance / Safety
> «Контроль и управление саморазвитием» (требование Славы). Двигатель жмёт газ — этот контур держит руль и тормоз.
**Принцип (ORCH-8, незыблемо):** самомодификация платформы (промпты/скиллы/конфиги агентов/ядро) — ТОЛЬКО через PR+ревью+апрув Славы. Орк ПРЕДЛАГАЕТ, ПРИМЕНЯЕТ через свой конвейер с гейтами.
**Уровни автономии (agentic AIOps maturity):**
| Уровень | Что авто | Гейт |
|---------|----------|------|
| L0 reactive | только алерт | человек делает всё |
| L1 assistive | предложить задачу+ТЗ | человек апрувит запуск |
| L2 autonomous-bounded | гонит безопасные классы (бэкенд-фиксы) до прода | safety-гейты CI/staging/regression |
| L3 self-modifying | менять агентов/ядро | **всегда** PR+апрув Славы, НИКОГДА не авто |
- **G1** Safety-политика L0-L3 + per-class правила (что можно само, что только через Славу). Лейблы autoApprove/autoDeploy (ORCH-89) = уже зародыш.
- **G2** Бюджет на саморазвитие: лимит $/мес, чтобы контур не жёг бесконтрольно.
- **G3** Дашборд эволюции: метрики 5 доменов в динамике — видно, КУДА развивается платформа.
- **G4** Kill-switch петли: остановить самогенерацию задач одним флагом.
---
## 10. 🌡️ Градусник автономности (сквозная метрика)
Объединяющая измеримая цель эпика. Каждый домен двигает её вверх:
- **% задач без ручного пинка** (сегодня было ~5 вмешательств: апрувы, домерж 063, sync 061).
- **Ручных вмешательств / неделю** (тренд вниз).
- **MTBF / MTTR** платформы (D1).
- **$/задача, токены/задача, время/задача** (D3).
- **Типов проектов/стеков поддержано** (D4).
- **Задач параллельно** (D5).
- **% уроков, ставших задачами** (двигатель).
---
## 11. Связь с Backlog (ничего не теряем)
| Backlog | Домен/вертикаль |
|---------|-----------------|
| ORCH-8 петля | 🧠 Двигатель (ядро) |
| ORCH-83 наблюдаемость | Фундамент F1 |
| ORCH-20/23/38/19 | 💰 D3 |
| ORCH-27/28 | ✅ D2 |
| ORCH-12/13/14/15/18/24/25 | 🚀 D4 |
| ORCH-9/10 | 📈 D5 |
| ORCH-94 флапп | 🛡️ D1.2 |
| ORCH-89 авто-лейблы | 🛑 G1 |
~18 backlog-задач ложатся в структуру. Эпик их систематизирует и достраивает.
---
## 12. Дорожная карта (предложение)
1. **Фаза 0 (фундамент):** F1 наблюдаемость + F2 журнал. Без них рулить нечем.
2. **Фаза 1 (две руки параллельно):**
- крепнем: D3.4 целевые файлы + D3.2 бюджет-breaker (дешёвый impact)
- растём: D4.1 стеки-плагины ИЛИ D4.4 интерактив-аналитик (по спросу)
3. **Фаза 2:** D1 надёжность (транзиент-резилентность, авто-ремедиация) + D2 качество + D5.1 параллелизм.
4. **Фаза 3 (мозг):** E2 ретроспективщик + E3 приоритизатор + G1 safety-политика → петля замыкается, дальше платформа предлагает сама.
---
## ⛓️ Реализация в Plane (решено 09.06)
**Ось ДОМЕНА → модули Plane** (1 задача = 1 модуль; slug в `external_id`, name с эмодзи для человека):
| Модуль (name) | slug (external_id) | module_id |
|---|---|---|
| 👁️ Фундамент | `foundation` | 74dee25a-a44b-4c3b-ab55-1b5638b8cc1f |
| 🧠 Мозг | `brain` | ab1afa08-14ce-4b7d-8ebc-e45ac19b2ba7 |
| 🛡️ Надёжность | `reliability` | abd7479e-4f9b-4a56-a926-cb2ece7558ca |
| ✅ Качество | `quality` | cbf5f8ca-dc1a-4dee-9d35-555459de2b30 |
| 💰 Экономика | `economy` | 9b4bbab3-95d6-4b8a-8d72-379a618ea2f3 |
| 🚀 Возможности | `features` | baa6936c-6a39-4935-ad57-31ef5ffc3041 |
| 📈 Масштаб | `scale` | 18373528-14fa-4627-a0f6-32497ff22177 |
**Ось ВЕРТИКАЛЬ → лейблы** (могут быть несколько, список короткий):
- `engine` (36f398f7-5a1c-4eeb-847a-56c457e1da6b) — задача пришла от петли/идеатора.
- `governance` (9eea4dd8-0fe7-473a-8c40-630fc3ab0d25) — требует апрува L3 / safety-внимания.
- (+ существующие `autoApprove`/`autoDeploy` — ортогональны, режим автономности.)
**Правило раскладки:** каждая задача эпика = 1 модуль-домен (по slug) + 0..N вертикаль-лейблов. Орк ищет/привязывает по `external_id` (не по русскому имени).
⚠️ **Порядок модулей на доске:** Plane API игнорирует `sort_order` на запись (только drag-and-drop в UI). Сейчас порядок перевёрнут (Масштаб сверху) — Славе поправить мышкой (фундамент→мозг→надёжность→качество→экономика→возможности→масштаб). На машинную логику не влияет (орк по slug).
---
## 13. Открытые вопросы Славе
1. **Структура Plane:** мега-эпик с фундаментом+5 доменами+2 вертикалями? Или эпик на каждый домен?
2. **D4 (возможности):** какой стек/фича приоритетны для тебя/заказчиков — Android, UX/UI, тяжёлые расчёты, интерактив-аналитик? С чего рост начинать?
3. **Баланс «крепнем vs растём»:** идти строго параллельно обеими руками, или в каждой фазе перевес в одну сторону?
4. **Safety L3:** подтверждаешь — самомодификация ядра/агентов всегда через твой апрув?
5. **Двигатель (E2/E4):** ретроспективщик + агент-идеатор сразу как агенты, или сначала Стрим ведёт журнал/банк идей вручную?
8. **Генератор идей (8B):** какие из источников I1-I7 тебе ценнее (гэпы проектов / тренды-технологии / конкуренты / саморефлексия Стрим)? Генерить автономно или только по твоему запросу?
6. **Бюджет на эпик (G2):** лимит $/мес?
7. **Первая задача** после апрува: F1 наблюдаемость, быстрая победа D3.4, или сразу рост D4.*?

View File

@@ -1,128 +0,0 @@
# Lessons Learned — 2026-06-05 (вечер): ORCH-17/45/47 + деплой прода
## Итог дня
Закрыты три задачи (ORCH-17, ORCH-45, ORCH-47), два прод-гейта стали умнее, заведено
4 системных задачи в бэклог (ORCH-44/46/48 + B6). Главный сквозной урок: **конвейер не мог
провести эти задачи автономно из-за дыр в самом конвейере** — потребовались ручные merge и
ребилды прода. Корни задокументированы, чинятся отдельными задачами.
---
## 1. ORCH-17 — approve-ping links (закрыта вручную)
Подробный разбор: `docs/history/LESSONS_ORCH-017.md`. Кратко: косметика (2 ссылки)
застряла 5 раз, объективный дедлок shared-гейта, ручной merge PR #37 (`26c6f267`).
---
## 2. ORCH-45 — CI-гонка в `check_ci_green` (исправлена, в проде)
### Проблема
`check_ci_green` делал **один** запрос статуса CI сразу после developer. Если CI ещё
`pending` 1-3 секунды (реальный кейс: опрос 17:58:54 → pending, CI позеленел 17:58:55) —
гейт возвращал False **один раз** и задача застревала насмерть с зелёным CI.
### Решение (PR #39, merge `982698c4`)
Поллинг с ретраем: `success`/`failure` — терминальны (сразу), `pending` → ждать
`CI_POLL_INTERVAL_S`(10с) до `CI_POLL_MAX_ATTEMPTS`(12) раз, истёк лимит → явный
`False` с причиной "CI still pending after Ns" (не виснет молча). Параметры в `config.py`
как env `ORCH_CI_POLL_*`. ADR-0004. +5 тестов (мок httpx + time.sleep).
---
## 3. ORCH-47 — тестер-гейт игнорил `result:` (исправлен, в проде)
### Проблема (уловка-22)
`check_tests_passed`/`_parse_tests_verdict` читал только `verdict:`/`status:` из frontmatter
`13-test-report.md`, но промпт tester-агента велит писать `result: PASS|FAIL`. Честный тестер
(`result: PASS`, без `verdict:`) → гейт «No machine-readable verdict» → ложный FAIL → петля
dev↔review↔tester → Blocked. **И сама ORCH-47 (которая это чинит) попала в тот же капкан:**
в проде крутился старый гейт → не понимал её собственный `result: PASS` → 3 круга петли.
Змея кусает хвост: чтобы пройти гейт автономно, фикс уже должен быть в проде.
### Решение (PR #40, merge `5d04de9e`)
`result:` добавлен как равноправное поле наряду с `verdict:`/`status:`. Любое одно непустое
поле достаточно. Negative-токен (BLOCKED/FAILED) в ЛЮБОМ поле авторитетен (ET-013 кейс
сохранён). Token sets заморожены для обратной совместимости. ADR-001. +6 тестов (68 passed).
После деплоя ручной `advance_stage` пнул застрявшую task → гейт принял `result: PASS`
прошёл testing. Петля исчезла навсегда.
### Остаточная находка → B6 / ORCH-48
На staging деплоер дал 9/10 PASS, завалил **B6 Registry isolation**: staging-реестр видит
боевые ET+ORCH вместо одного sandbox (нарушает «staging — только sandbox»). Деплоер честно
поставил FAILED и НЕ стал натягивать зелёнку (вне мандата) → откат by design. К фиксу гейта
отношения не имеет (E2E против sandbox прошёл). Заведена ORCH-48.
---
## 4. ДЕПЛОЙ ПРОДА — как правильно (важная операционная памятка)
### `/app` запечён в образ, НЕ volume
`docker-compose.yml`: `build: .` + `COPY src/ ./src/`. Поэтому `git pull` + рестарт с
`--no-build` **НЕ довозит код** — нужен `docker compose build orchestrator`. Деплой-хук
(`scripts/orchestrator-deploy-hook.sh`) по дефолту целит в **staging** (by design) — для
прода нужны env `TARGET_SERVICE=orchestrator TARGET_PORT=8500 COMPOSE_PROFILE=''`.
### Порты/профили
- prod orchestrator = порт **8500** (`/health``{"status":"ok"}`), `network_mode: host`,
профиль prod = пустой (стартует обычным `docker compose up -d orchestrator`).
- staging = порт **8501**, профиль `staging` (стартует только `--profile staging`).
### Рабочая последовательность деплоя (проверена дважды 05.06)
1. `sudo chown -R slin:slin /home/slin/repos/orchestrator` (см. грабля ниже).
2. `git checkout main && git reset --hard origin/main && git clean -fd -e '*.bak*' -e '.deploy-prev-image-prod'`.
3. `docker compose build orchestrator`.
4. `docker compose up -d orchestrator` + health-loop на :8500.
5. **Проверка claude-auth** (ребилд её ломает — см. ниже).
6. Проверка что новый код активен в `/app` (grep маркера правки).
### ⚠️ ГРАБЛЯ: хост-репо рассинхронизирован с git (агенты пишут под root)
Хост-репо `/home/slin/repos/orchestrator` оказывался на feature-ветке (не main), а рабочая
копия засеяна untracked+modified файлами, созданными агентами **под uid=0 (root-owned)** прямо
в репо. → `git pull --ff-only` падал `Permission denied` / `would be overwritten`, обычный
`rm` под slin не мог снести root-файлы. **Лечение:** `sudo chown -R slin:slin <repo>`
проверить что modified=совпадает-с-main и untracked=уже-в-main (дубликаты, не теряем) →
`git reset --hard origin/main` + `git clean`. **Хук это НЕ разруливает** — сверять состояние
хост-репо перед каждым деплоем.
### ⚠️ ГРАБЛЯ: ребилд ломает claude-auth (проверять ВСЕГДА)
Пересоздание контейнера может root-овнить `/home/slin/.claude/.credentials.json` и сделать
`/root/.claude` пустышкой → агенты падают `Not logged in`. Защита — монтирование creds в
compose (`/home/slin/.claude` + `.claude.json`), launcher форсит `HOME=/home/slin`.
**После каждого ребилда боевая проверка:**
`docker exec orchestrator bash -c 'cd /tmp && HOME=/home/slin /opt/claude-code/bin/claude.exe --print "ОК"'`
(timeout 90с). 05.06 auth пережил оба ребилда — защита держит.
---
## 5. ЗАПУСК конвейера и Gitea API
### Старт конвейера = Plane Backlog → In Progress
Конвейер стартует штатно переводом задачи в Plane из Backlog в **In Progress** (код:
`webhooks/plane.py handle_status_start` — «pipeline is started when Slava moves the issue
into In Progress»). Webhook создаёт task-row, заводит ветку, запускает analyst. Никаких
ручных вставок в БД.
### QG-0: лимит заголовка 80 символов
При старте задача с заголовком >80 символов заворачивается на QG-0 («Title слишком длинный»)
и уходит в Blocked. Чинить — укоротить `name` (суть в заголовок, детали в description),
вернуть в Backlog, снова In Progress.
### Gitea API грабли
- **merge/create PR** требуют заголовок `Authorization: token <ORCH_GITEA_TOKEN>` (форма
с префиксом `token `), иначе 401 "token is required".
- **heredoc через ssh+docker exec глотает вывод** python-скрипта. Надёжный путь: написать
`.py` локально → `base64 -w0``ssh "echo <b64> | base64 -d > /tmp/x.py"``docker cp`
`docker exec python3 /tmp/x.py`. Это же обходит экранирование кириллицы/скобок.
---
## Состояние прод-гейтов после 05.06
-`check_ci_green` — поллинг с ретраем (ORCH-45)
-`check_tests_passed` — читает `result:`/`verdict:`/`status:` (ORCH-47)
## Бэклог (high) после дня
- **ORCH-44** — надёжность запуска агента (preflight слеп к auth; `--effort` гасит вывод;
пустой run-лог → должен быть failed).
- **ORCH-46** — «испорченный телефон»: орк не передаёт деву ТЕКСТ замечаний reviewer/tester
(только ссылку на файл), противоречивые сигналы tester↔reviewer, нет памяти между кругами.
- **ORCH-48 / B6** — staging registry isolation (staging видит прод-проекты вместо sandbox).

View File

@@ -1,78 +0,0 @@
# Lessons Learned — 2026-06-07: замыкание автономности self-deploy (5 задач в прод)
## Итог
За одну сессию закрыты в прод **5 задач**, завершающих автономный self-deploy эпика ORCH-54:
| Задача | Что | Прод-коммит |
|--------|-----|-------------|
| ORCH-58 | provenance retag-guard (свежесть staging-образа перед BUILD-ONCE) | 094b5e2 |
| ORCH-60 | reconciler не трогает escalated/Blocked/Needs-Input | d4c6cc0 |
| ORCH-61 | фикс петли deploy-staging (staging_verdict: waive sandbox-infra FAILs C9a/C9b) | e18947d |
| ORCH-21 | post-deploy мониторинг прода + auto-rollback (self-hosting=alert-only) | f85e449 |
| ORCH-65 | job-reaper + stale merge-lease reclaim + idempotent merge | bb03350 |
**Главное:** после ORCH-60/61 конвейер впервые провёз задачи (ORCH-21/65) через deploy-staging
**автономно** без отката; после ORCH-65 (job-reaper в проде) зомби-job и зависшие merge-lease
лечатся сами. Последняя ручная точка автономного деплоя закрыта.
---
## Класс багов: «процесс умер — ресурс захвачен навсегда» (ORCH-65)
Три связанных отказа, все воспроизвелись на ORCH-58/60/61/21:
- **zombie jobs:** агент завершился/умер, строка jobs осталась running. requeue_running_jobs()
спасает только на старте процесса; зомби без рестарта не лечился → при concurrency=1 встаёт
конвейер ВСЕХ проектов. (jobs 236/239/242/254/265 — все зомби за сессию.)
- **stale merge-lease:** merge-gate берёт .merge-lease-<repo>.json, делает rebase+re-test green,
а на финальном merge процесс умирает с зажатым lease → merge не докатывается.
- **неидемпотентный merge:** re-drive повторно пытается слить уже слитый PR.
Фикс: фоновый job_reaper (паттерн reconciler, dead_ticks streak + мёртвый pid + exit_code,
атомарный reap-claim, never-raise, kill-switch, снимок в /queue) + проактивный lease-reclaim
по pid + guard pr_already_merged ПЕРЕД merge.
## Петля deploy-staging (ORCH-61) — ДВЕ причины
1. ложный check_staging_status FAILED: staging_check падает на C9a/C9b (sandbox e2e branch +
analyst-job-in-queue), т.к. bot-токены SANDBOX-проекта не настроены — НЕ регресс кода.
2. no-changes для action-стадий (деплой = рестарт/retag, не правка → коммитить нечего).
Фикс: staging_verdict waive sandbox-infra-only FAILs.
## Инфра-каскад от переполненного диска (инцидент дня)
- Частые build-once/--build-staging пересборки за день забили docker build cache до 11 ГБ →
диск 100% → CI red (No space left).
- ДАЖЕ после чистки диска Gitea осталась в сломанном состоянии: внутренняя queue
(/data/gitea/queues/common/*.log) залипла → post-receive hook 500 → actions tasks НЕ
создаются, CI не триггерится вовсе (статус пустой, не failure). runner при этом online+idle.
- Лечение: docker builder prune -af + рестарт Gitea (queue распускается → CI ожил).
---
## Уроки
1. **Self-hosting safety (сквозной принцип):** прод-орк обслуживает ВСЕ проекты. Нельзя авто-
откатывать/рестартить self в рамках задачи; нельзя пушить main. ORCH-21 post-deploy для
self-hosting = alert-only, авто-rollback только для не-self репо.
2. **TDD без доводки (повтор ORCH-58 и ORCH-65 v1):** тесты есть, реализация/wiring не
подключены к боевому пути → мёртвый код + врущая дока. Reviewer обязан грепать вызовы из
прод-кода, не только наличие функции.
3. **Concurrency-баги ловятся итеративно:** ORCH-65 3 прохода reviewer (мёртвый guard → race
condition side-effects-before-claim → approve) — каждый раз НОВЫЙ реальный дефект, не
зацикливание. Atomic-claim ДО side-effects — обязательное правило.
4. **При красном CI + зелёных локальных тестах — ПЕРВЫМ делом df -h / и docker system df**,
не копаться в коде. После disk-full обязателен рестарт Gitea (queue залипает).
5. **Bootstrap-разрыв:** задача про автономность деплоя не может задеплоить себя автономно,
пока её механизм не в проде. Последний прод-деплой каждого такого фикса — вручную.
6. **Перед прод-retag (build-once SOURCE_IMAGE=staging):** проверить revision-label staging-
образа == целевой main HEAD, иначе guard fail-closed (by design). Если != → пересобрать
--build-staging GIT_SHA=<main HEAD>.
## Ручная доводка прод-deploy (схема до ORCH-65 в проде)
cancel zombie job → park task In Progress → merge PR (Gitea pulls/{n}/merge Do=merge, CI green)
→ --build-staging GIT_SHA=<main HEAD> (проставит label) → rollback-снимок → --deploy с
EXPECTED_REVISION=<sha> (guard сверит → retag → health 200) → Plane Done + UPDATE tasks stage=done.
## Follow-up (Backlog)
- ORCH-62: авто-prune docker build cache (cron/daemon.json defaultKeepStorage).
- ORCH-63: мониторинг диска mva154 + алерт >85%.
- ORCH-64: починить NTP/часы mva154 (ушли ~+3ч от UTC).
## Осталось в эпике ORCH-54
ORCH-22 (security-гейт), ORCH-59 (Confirm Deploy статус), ORCH-23 (budget circuit-breaker),
P2: ORCH-57, ORCH-51.

View File

@@ -1,33 +0,0 @@
# 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

@@ -1,47 +0,0 @@
# 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

@@ -1,103 +0,0 @@
# Lessons Learned — ORCH-017 (Telegram approve-ping links)
## Дата: 2026-06-05
## Задача: ORCH-017 — Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве BRD
## Итог: смержено **вручную** (PR #37, merge `26c6f267`) после ~5 застреваний конвейера
---
## TL;DR
Косметическая задача (две HTML-ссылки в уведомлении) **5 раз застряла** в конвейере и каждый
раз требовала ручного пинка. Корень — **не баг задачи, а дыры автономности конвейера**. Код был
готов и зелёный (434 теста), но пайплайн не мог довести его до merge сам. В итоге — ручной merge
Owner-ом; четыре системные дыры заведены в бэклог (ORCH-44/45/46/47).
---
## Хронология застреваний
1. **Auth claude после rebuild** — агенты падали `Not logged in` (root-owned creds + `/root/.claude`
пустышка). См. отдельный разбор в memory/INCIDENT по auth. → починено + защищено монтированием.
2. **`check_ci_green` race** — гейт опросил CI **один раз** в `17:58:54``pending`; CI дозеленел в
`17:58:55` (промах на 1 секунду). Повторного опроса нет → задача висит насмерть с зелёным CI.
3. **Петля dev↔review↔testing → `max retries reached`** (MAX_DEVELOPER_RETRIES=3).
4. **Откат неполный** — убрали код shared-гейта, но оставили 2 doc-строки про него → рассинхрон
код↔доки → reviewer снова REQUEST_CHANGES.
5. **Объективный дедлок** (см. ниже) → ручной merge.
---
## Корневые проблемы (→ бэклог)
### P1. `check_ci_green` промахивается на гонке CI (→ ORCH-45)
Гейт читает статус CI **ровно один раз** сразу после developer. Если CI ещё `pending` — задача
застревает молча, без повторного опроса. Нужен polling с ретраем: `pending` → ждать N×15с,
`success` → advance, `failure` → rollback, вечный `pending` → уведомить (не застревать молча).
### P2. Developer не понимает замечаний reviewer/tester — "испорченный телефон" (→ ORCH-46)
**Это прямой удар по автономности.** Три причины, почему dev повторял одну и ту же ошибку:
- **Испорченный телефон.** При REQUEST_CHANGES `stage_engine.py:~421` шлёт developer-у только
`"Fix findings in docs/work-items/<WI>/12-review.md"`**без текста претензий**, лишь ссылку на
файл. Ключевую governance-мысль легко проскочить. → Вклеивать ТЕКСТ findings прямо в task_desc.
- **Противоречивые сигналы.** После tester прилетает `"Tests FAILED. Fix failures"` (толкает чинить
связанное с тестами → dev лез в test-gate). После reviewer — `"не трогай gate"`. Два
противоположных приказа. → Склеивать замечания tester+reviewer в одно непротиворечивое ТЗ.
- **Нет памяти между кругами.** Каждый запуск developer — новый чистый агент, не помнит прошлых
заворотов. Видит "тесты падают" → снова лезет в gate. → Передавать историю прошлых REQUEST_CHANGES/
FAIL ("на чём уже погорел, чего НЕ делать"). Можно: ранняя эскалация к Owner при повторе.
### P3. `check_tests_passed` игнорирует поле `result:` (→ ORCH-47)
`_parse_tests_verdict` (`src/qg/checks.py`) читал только `verdict:`/`status:` из frontmatter
`13-test-report.md`. НО промпт tester-агента (`.openclaw/agents/tester*`) предписывает писать
`result: PASS | FAIL`. Честный тестер (отчёт ORCH-017: `result: PASS`, без `verdict:`/`status:`)
проваливал гейт ложным «Tests FAILED» → откат на development. ORCH-016 проходил лишь потому, что
дублировал `verdict:` И `result:`. → Гейт должен читать `result:` как первоклассное машинное поле.
**ВАЖНО:** это shared-гейт (влияет на ВСЕ проекты общего прода) → требует отдельного ADR
(CLAUDE.md правило 2), потому вынесено в свой work item, не в ORCH-017.
### P4. Preflight слеп к auth и битым флагам (→ ORCH-44)
`claude --version` отвечает даже без логина → preflight=ok, а реальный запуск падает `Not logged in`.
Плюс `--effort` с CLI 2.1.142 + `--print`/`--output-format json` гасит вывод. Нужны: дешёвая
проверка auth без токенов (права+дата истечения OAuth в `.credentials.json`), фикс effort,
«пустой лог + job running + процесс мёртв → failed».
---
## Главный урок: объективный дедлок shared-инфры
ORCH-017 попала в **неразрешимый автономно дедлок** из-за того, что тест-отчёт уже написан под
новый контракт (`result: PASS`):
- **С фиксом гейта в ветке** → reviewer заворачивает (governance: shared-инфра без ADR). ❌
- **Без фикса гейта** → `check_tests_passed` не видит `result:` → ложный FAIL → откат. ❌
**Вывод:** изменение shared quality-gate нельзя протаскивать внутри прикладной задачи. Оно создаёт
циклическую зависимость (артефакты задачи зависят от изменённого гейта, а гейт нельзя менять без
отдельного ADR). Менять shared-гейты — только отдельным work item со своим ADR. Если артефакты уже
написаны под новый контракт — задача физически не пройдёт, пока не приедет фикс гейта.
---
## Урок про роль ассистента/оператора
Когда оператор **раз за разом пинает гейты и чистит за dev вручную** — это сигнал «конвейер не тянет
автономно». Честнее предложить Owner-у ручной merge/эскалацию, чем гонять карусель кругов и доказывать
конвейеру то, что уже готово (код зелёный, reviewer: «технически корректно», претензии процедурные).
---
## Урок про откат
При откате **кода** обязательно откатывать и **доки/CHANGELOG**, иначе возникает обратный
рассинхрон код↔доки (доки описывают фичу, которой в коде уже нет) → reviewer заворачивает. Откат —
это код + доки + changelog + (при необходимости) тест-отчёт одним согласованным движением.
---
## Что сработало хорошо
- **Reviewer ловит governance-нарушения** — корректно завернул протаскивание shared-гейта в
прикладную задачу. Процедурно прав, даже когда код технически верный.
- **Безопасный ручной пинок гейта** через `stage_engine.advance_stage(...)` — без ребилда/мержа,
перевызывает QG внутри процесса орка.
- **Ручной merge как осознанный выход** из дедлока (с явным ОК Owner), а не бесконечные круги.

View File

@@ -1,120 +0,0 @@
# Lessons Learned — 2026-06-06 (вечер): ORCH-36 + ORCH-53 → прод (эпик ORCH-54)
## Итог
Закрыты две задачи эпика ORCH-54 (автономное внедрение): **ORCH-36** (исполняемый
самодеплой стадии `deploy`) и **ORCH-53** (sweeper/reconciler потерянных webhook).
Обе прошли конвейер через рабочий merge-gate (ORCH-43), но финальный мерж+деплой
потребовал **ручного разрыва bootstrap-цикла** — задача, добавляющая автодеплой, сама
не может задеплоить себя через старую логику. Reconciler доказал себя **в первую секунду
после старта** — разблокировал две реально застрявшие задачи (ORCH-036 и ET-013).
Эпик ORCH-54: **4 из 6 в проде** (ORCH-40 права, ORCH-43 merge-gate, ORCH-36 деплой,
ORCH-53 reconciler). Осталось: ORCH-51 (окно/HA), обкатка полностью автономного деплоя.
---
## 1. 🔴 Bootstrap-парадокс самодеплоя (ORCH-36)
### Симптом
ORCH-36 застряла в петле `deploy → development`:
```
QG check_deploy_status — failed: Deploy log not found (14-deploy-log.md)
→ deployer verdict FAILED, rolled back deploy → development
```
deployer запускался (exit 0), но **не писал** `14-deploy-log.md` → гейт FAILED → откат →
снова deployer → бесконечный цикл (jobs 140→142→143...).
### Корень
Классический bootstrap самохостинга: **новая deploy-логика лежит в ветке, старая работает
в проде**. ORCH-36 учит deployer писать лог по результату РЕАЛЬНОГО деплоя (через хост-хук),
но прод-deployer работает по СТАРОМУ промпту, который для self-репо реального деплоя не делает
и SUCCESS-лог не пишет. Нет лога → FAILED → откат.
### Урок
**Self-репо не может задеплоить сам себя через старую логику.** Нужен разовый ручной разрыв
цикла: домержить + задеплоить руками ОДИН раз, дальше конвейер катит своей же новой логикой.
Тот же паттерн был у ORCH-40/43. Это структурное свойство любой задачи, меняющей
deploy/merge-механику самого оркестратора — закладывать ручной bootstrap-шаг в план.
---
## 2. 🔴 Merge-конфликт при последовательном ручном мерже двух задач
### Симптом
PR #56 (ORCH-53) смержен первым — чисто. PR #55 (ORCH-36) сразу после → **CONFLICT 409**:
`.env.example`, `CHANGELOG.md`, `docs/architecture/README.md`, `docs/operations/INFRA.md`,
**`src/config.py`**.
### Корень
После мержа PR #56 `main` ушёл вперёд → PR #55 валидировался против СТАРОГО main (точки
ответвления), а мержится в НОВЫЙ. Это ровно класс «main ушёл вперёд», который чинит
merge-gate (ORCH-43) — но при РУЧНОМ мерже через Gitea API merge-gate не участвует.
### Решение
- **merge main→ветку, НЕ rebase.** Rebase 9 коммитов = 9 потенциальных конфликт-разборов;
один merge-коммит = ОДИН разбор. Быстрее и безопаснее для большого набора коммитов.
- Конфликт в `src/config.py` был чисто **аддитивный**: ветка ORCH-36 добавляла блок
`self_deploy_*` настроек, main (ORCH-53) — блок `reconcile_*`. Нужны **ОБА** блока →
склеить, убрав только git-маркеры (`<<<<<<<`/`=======`/`>>>>>>>`). Обязательно после —
`python3 -c 'import ast; ast.parse(...)'` для проверки синтаксиса.
- docs/.env/CHANGELOG конфликты — тоже аддитивные (обе стороны добавляют строки) → union.
### Грабли
⚠️ `grep -rE '^(<<<<<<<|=======|>>>>>>>)'` по `docs/work-items/*/13-test-report.md` даёт
**ЛОЖНЫЕ срабатывания** — там `=======` это markdown-разделители таблиц/секций, не
git-конфликты. Проверять реальные конфликтные файлы поимённо, не доверять глобальному grep.
---
## 3. Review-гейт поймал 2 реальных P1 ДО прода (ORCH-36)
reviewer завернул первую версию (`verdict: REQUEST_CHANGES`), конвейер сам откатил
dev→review→fix→APPROVED. Два P1:
1. **sentinel-маркеры self-deploy (`initiated`/`result`/`approve-requested`) не чистились на
rollback** → при возврате задачи человек ставит Approved, а устаревший маркер ломает фазу B.
2. **нет `.env.example` для новых флагов** + процедуры «approve→деплой» в `INFRA.md`.
Урок: merge-gate + review отрабатывают как задумано — брак не уходит в прод автономно.
Это и есть ценность эпика: система фильтрует сама.
---
## 4. 🔥 Reconciler доказал себя мгновенно (ORCH-53)
В первую секунду после рестарта прода (21:24 UTC):
```
reconciler: ORCH-036 development разблокирована (потерян webhook)
reconciler: ET-013 development разблокирована (потерян webhook)
```
Sweeper нашёл и разблокировал ДВЕ реально застрявшие задачи — включая саму ORCH-036 из
bootstrap-петли, и старое зависание ET-013 (enduro-trails). Ручной heartbeat-watchdog,
который раньше держал Стрим, **больше не нужен** — система чинит застревания сама.
---
## 5. Операционные мелочи (закрепить)
- **Заголовки ORCH-задач ≤80 символов.** QG-0 (`check title length`) заворачивает старт
конвейера, если длиннее. ORCH-53 был 83 символа → завернул на старте → подрезали до 71.
- **Developer-таймаут 1800с (30 мин) мал для мясных задач.** 1-й заход developer'а ORCH-36
(деплой-хук + Telegram-кнопка + callback) упёрся в лимит → SIGKILL (exit -9). Спас
resilience-ретрай (ORCH-1b): attempt 2, наработки в worktree между попытками сохранились.
Если упирается систематически — поднять `agent_timeout_seconds` (override per-agent) или
дробить задачу.
- **Время хоста ≠ UTC.** Файлы worktree датируются по мск (+3), БД/системное — UTC. Не баг,
но путает сверки `etime`/`updated_at`/`finished_at`. Сверять по одному источнику.
- **Gitea merge auth:** заголовок строго `Authorization: token <ORCH_GITEA_TOKEN>` (формат
`token `, буквально). НЕ маскировать токен плейсхолдером `***` → иначе 401.
POST `/repos/admin/orchestrator/pulls/{N}/merge`, body `{"Do":"merge"}`.
- **approve прод-деплоя 8500 = Telegram-кнопка** (решение Owner), флаг
`DEPLOY_REQUIRE_MANUAL_APPROVE=true` по дефолту.
- **max_concurrency=1 оставлен сознательно** (решение Owner): одна БД/очередь на все
проекты, последовательное выполнение надёжнее. НЕ поднимать без явного запроса.
---
## Состояние прода после деплоя (21:24 UTC, main `1ff8d85`)
- `src/self_deploy.py` — в проде (исполняемый деплой, 3 фазы A/B/C)
- `src/reconciler.py` — в проде (фоновый sweeper, уже разблокировал 2 задачи)
- uid 1000, health `{"status":"ok"}`, preflight True (Claude Code 2.1.142)
- Деплой-скрипт с авто-rollback: исходник в workspace `temp/deploy_36_53.sh`

View File

@@ -1,78 +0,0 @@
# Lessons Learned — 2026-06-07 (утро): ORCH-36 self-deploy bootstrap — каскад неготовности инфры
## Итог
ORCH-36 (исполняемый самодеплой стадии `deploy`) **замкнулась в проде** — конвейер
впервые задеплоил сам себя по полному циклу Phase A→B→C (approve → детачед ssh-хук →
finalizer). Но путь до Done вскрыл **четыре слоя неготовности инфраструктуры**, каждый из
которых требовал ручного bootstrap-разрыва: задача про автодеплой не может задеплоить
сама себя, пока её же механизм + инфра не в проде.
Эпик ORCH-54: **4/6 в проде** (ORCH-40 права, ORCH-43 merge-gate, ORCH-36 самодеплой,
ORCH-53 reconciler). Конвейер автономен: мержит → катит в прод → чинит застрявшее.
---
## Каскад из 4 инфра-багов (все вскрылись только при РЕАЛЬНОМ деплое)
### 1. 🔴 uid 1000 без записи в `/etc/passwd` → ssh/whoami падают
**Симптом:** `self-deploy initiate failed: ssh launch failed (rc=255): No user exists for
uid 1000`. **Корень:** регрессия ORCH-40 — compose запускает контейнер под `1000:1000`,
но базовый образ `python:3.12-slim` не имеет passwd-записи для 1000. SSH-клиент (и
`whoami`, `getpwuid()`) отказываются стартовать без валидного юзера.
**Фикс:** в `Dockerfile``groupadd -g 1000 app && useradd -u 1000 -g 1000 -m -d
/home/slin -s /bin/bash slin`. Rebuild + recreate. Коммит `64e031a`.
**Урок:** при переводе контейнера на non-root uid (ORCH-40) ОБЯЗАТЕЛЬНО создавать passwd-
запись в образе, иначе ssh/git/любой инструмент с getpwuid() ломается. Проверять
`docker exec <c> whoami` после смены uid.
### 2. 🔴 env-префикс: `DEPLOY_*` vs `ORCH_DEPLOY_*` (pydantic не видит)
**Симптом:** `ssh: Could not resolve hostname : No address associated with hostname`
host пустой, хотя в compose `DEPLOY_SSH_HOST=127.0.0.1` задан. **Корень:** `Settings`
имеет `env_prefix = "ORCH_"` → читает ТОЛЬКО `ORCH_DEPLOY_SSH_HOST`. Старые
`DEPLOY_*` (без префикса) предназначались легаси enduro-деплоеру (читает через
`os.environ` напрямую) и pydantic их игнорирует → дефолт `host=""`. Доп: `DEPLOY_HOOK_SCRIPT`
указывал на `enduro-deploy-hook.sh`, не на orchestrator-хук.
**Фикс:** в `docker-compose.yml` добавлены `ORCH_DEPLOY_SSH_USER/HOST`,
`ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh`,
`ORCH_DEPLOY_HOST_REPO_PATH` (легаси `DEPLOY_*` оставлены для enduro). Коммит `115519e`.
**Урок:** все настройки, читаемые через pydantic Settings, ДОЛЖНЫ иметь префикс `ORCH_`.
Проверять резолв: `docker exec <c> python3 -c 'from src.config import settings; print(settings.deploy_ssh_host)'`.
### 3. 🔴 `/var/log/orchestrator` принадлежит root → хук падает на tee
**Симптом:** `tee: /var/log/orchestrator/deploy-hook.log: Permission denied`, хук exit 1.
**Корень:** лог-директория `root:root`, а хук бежит под `slin`. **Фикс:** `chown -R
slin:slin /var/log/orchestrator` на хосте.
**Урок:** все пути, в которые пишет хост-хук (логи, sentinel, prev-image), должны быть
writable юзером, под которым ssh-сессия. Заложить создание/chown в provisioning хоста.
### 4. 🔴🔴 BUILD-ONCE retag берёт УСТАРЕВШИЙ staging-образ → катит регресс (ВАЖНО)
**Симптом:** деплой «зелёный» (result=0, health ok), но прод откатился на код 2-дневной
давности — пропал `deploy-finalizer` (`Unknown agent: deploy-finalizer`), задача не
закрылась. **Корень:** хук делает `BUILD-ONCE: retag orchestrator-orchestrator-staging →
orchestrator-orchestrator` (без rebuild, by design ORCH-36 BR-6). Дизайн предполагал
«staging-образ = свежий, провалидированный». В РЕАЛЬНОСТИ `orchestrator-orchestrator-staging`
никто не пересобрал из нового main → retag катил в прод СТАРЫЙ образ → бесконечная петля:
каждый Phase B возвращал прод в прошлое, finalizer (новый код) исчезал, Phase C не мог
закрыть задачу.
**Фикс (ручной разрыв):** пересобрать `orchestrator-orchestrator-staging` из актуального
main ПЕРЕД retag → тогда хук катит свежий код. После этого Phase C отработал: result=0 →
SUCCESS → `deploy → done`.
**Урок / ТЕХДОЛГ:** retag-стратегия BUILD-ONCE предполагает гарантию свежести staging-
образа, которой НЕТ. Нужна отдельная задача: либо staging-деплой пересобирает образ из
текущего main перед валидацией, либо deploy-хук проверяет, что staging-образ собран из
HEAD main (по labels/sha), иначе fail-fast. Сейчас «зелёный» деплой может молча катить
регресс. **Это самый опасный из четырёх — он не падает, а тихо откатывает прод.**
---
## Сквозной урок: bootstrap самохостинга
Любая задача, меняющая deploy/merge-механику САМОГО оркестратора, упирается в парадокс:
её механизм не работает, пока не в проде, а в прод его можно влить только старым
механизмом. Каждый слой (код → права → env → образ) вскрывается ТОЛЬКО при первом
реальном прогоне. Закладывать в план таких задач **ручной bootstrap-чеклист** и гонять
**реальный** деплой в staging-петле до мержа, а не только бумажные гейты.
## Прод после (main `115519e`+, образ 2026-06-07 09:47)
- self_deploy.py + reconciler.py в проде, finalizer зарегистрирован (grep=5)
- uid 1000 = slin (passwd ok), ssh slin@127.0.0.1 работает, /var/log/orchestrator writable
- ORCH-36 task 43 → done, Plane → Done

View File

@@ -1,119 +0,0 @@
# LESSONS — ORCH-048 (B6 staging registry isolation, вариант «в»)
**Дата:** 2026-06-06
**Work item:** ORCH-048 — «staging B6 check reads registry from host worktree, not staging container»
**Статус:** ✅ Done. Merge PR #45 (`2a36ed80`), Plane → Done, task 38 → done. Прод не тронут.
---
## TL;DR
B6-чек staging-suite давал **ложный FAIL** (`prod-ET=YES, prod-ORCH=YES`), блокируя `deploy-staging` у **всех** ORCH-задач, хотя изоляция реестра в staging работала корректно. Починили, выбрав архитектурный вариант, который **не порождает новых ловушек автономности**. По дороге словили три урока, которые стоят дороже самой фичи.
---
## 1. Root cause (для истории)
`scripts/staging_check.py` блок **B6** был единственным чеком suite, который не ходил по HTTP к живому инстансу, а **импортировал Python-код локально**:
```python
sys.path.insert(0, "/repos/orchestrator") # host-worktree
importlib.reload(sys.modules["src.projects"]) # подхватывает env ТЕКУЩЕГО процесса
known = known_plane_project_ids()
```
Деплоер запускал suite **с хоста**, где `ORCH_PROJECTS_JSON` не задан → `src.projects` грузил встроенный `_DEFAULT_PROJECTS` (ET+ORCH) → `known_plane_project_ids()` возвращал боевые id → **ложный FAIL**. То есть B6 проверял реестр НЕ того окружения, реестр которого реально использует staging-инстанс.
Изоляция при этом была исправна: внутри `orchestrator-staging` `known_plane_project_ids()` корректно отдавал только sandbox (`.env.staging`).
---
## 2. ГЛАВНЫЙ УРОК: «курица-яйцо» в staging-гейте
Архитектор на первом прогоне выбрал **вариант (а): новый HTTP-эндпоинт `GET /projects`**, и B6 стал ходить на него. Решение красивое (единый HTTP-стиль с остальными чеками), **но оно само себя заблокировало**:
- B6 проверяет **работающий** staging-инстанс (порт 8501).
- Эндпоинт `/projects` **запечён в Docker-образ** (`src/main.py`).
- В текущем (ещё не пересобранном) образе эндпоинта НЕТ`GET /projects`**404** → B6 FAIL → откат на development.
- Чтобы чек прошёл, нужен **ручной bootstrap-деплой** образа. А деплой не происходит, потому что чек красный. **Тупик by design.**
Подтверждено на проде: `GET /projects` на 8501 и 8500 → 404 → `deploy-staging FAILED`.
**Вывод-правило:**
> Staging-чек НЕ должен проверять то, что появляется в работающем инстансе только ПОСЛЕ деплоя проверяемой ветки. Иначе первый прогон всегда падает и требует ручного bootstrap — это прямая поломка автономности.
**Решение — вариант (в):** запускать suite **ВНУТРИ** staging-контейнера (`docker exec orchestrator-staging`), читать реестр из собственного process-env контейнера, убрать host-path хак. Преимущество принципиальное:
- B6 не зависит от того, что отдаёт инстанс по HTTP.
- `staging_check.py` берётся из bind-mount → свежий код подхватывается **без ребилда образа**.
- **Курицы-яйца нет ни на первом прогоне, ни в будущем.**
Вариант (б) (`docker exec ... python3 -c "..."` + парсинг stdout) отклонён: хрупкое экранирование (см. `LESSONS_2026-06-05.md`).
**Как это попало в реализацию:** после FAIL под (а) — откатили ветку к analyst-артефактам (`git reset --hard <analyst-commit>`), стёрли ADR(а)+код(а), зашили в `02-trz.md §4` блок «РЕШЕНИЕ ПРИНЯТО ВЛАДЕЛЬЦЕМ: вариант (в)» с обоснованием и чек-листом, откатили задачу на `architecture` + поставили job архитектору заново. Второй прогон: arch→dev→review→tester→deploy-staging — без петель, **B6 ✓ PASS, 10/10**.
---
## 3. УРОК: орк мержит в main ТОЛЬКО логи, а не фикс-код
После прохождения staging орк сам:
- закрыл задачу в `done`,
- смержил в `main` PR с **логами** (`15-staging-log.md`, `14-deploy-log.md`),
- но **сам фикс-код остался в feature-ветке**`main` всё ещё содержал старый сломанный B6.
Это by design: фичу в main вливает **владелец**. Поймали проверкой:
```bash
git fetch origin -q
git log --oneline origin/main..origin/feature/<branch> # покажет невлитые коммиты фикса
git show origin/main:scripts/staging_check.py | grep -c '_evaluate_b6' # 0 = фикс НЕ в main
```
**Правило:**
> Прежде чем считать задачу реально доставленной — проверить `git log origin/main..feature` и наличие ключевой функции/строки фикса в `origin/main`. `done` в Plane + смерженные логи ≠ код в main.
Финальный шаг: смерджить feature-PR в main (Gitea API, `Do: merge`), затем синхронизировать host-репо.
---
## 4. УРОК: rollout bind-mount-фикса = host `git pull`, без ребилда/рестарта прода
ORCH-048 менял только **bind-mounted / non-runtime** артефакты:
| Файл | Как доходит до прода |
|------|----------------------|
| `scripts/staging_check.py` | bind-mount (`/home/slin/repos``/repos`); **не** в образе (`scripts/` нет в `/app`) → host `git pull` → live сразу |
| `.openclaw/agents/deployer.md` | bind-mounted промпт, читается при запуске агента → live на следующем запуске |
| `tests/`, `docs/` | не деплоятся |
`src/` и `Dockerfile` НЕ менялись → **рестарт/ребилд прод-контейнера 8500 не нужен и не делался** (zero group-risk для ET).
**Грабли host-репо:** `git pull` в `/home/slin/repos/orchestrator` сначала упёрся в `sudo: a password is required` — ложная тревога. Репо принадлежит `slin`, sudo не нужен; прямой `git pull --ff-only origin main` прошёл. **Сначала проверь `ls -ld` / `stat -c %U` репо — не лезь в sudo вслепую.**
**Верификация rollout в живом bind-mount (обязательна):**
```bash
grep -c '_evaluate_b6' scripts/staging_check.py # >=1
grep -c 'sys.path.insert(0, "/repos/orchestrator")' scripts/staging_check.py # 0
grep -c 'docker exec orchestrator-staging' .openclaw/agents/deployer.md # >=1
curl -s -o /dev/null -w '%{http_code}' http://localhost:8500/health # 200
```
---
## 5. Технические заметки (gotchas)
- **В контейнере orchestrator НЕТ `curl`** — для Gitea/Plane API использовать `urllib` через python (script-file → base64 → `docker cp``docker exec`).
- **Plane state-id зависят от проекта.** Approved для проекта orchestrator = `63f2c8fe-dcda-4ace-952f-dd88bd0118ff` (НЕ дефолтный `a519a341...` из кода — тот для sandbox/ET). Брать реальные state-id через `GET .../states/`.
- **BRD-апрув = перевод Plane-issue в статус Approved** → webhook ловит смену статуса → путь `agent=None``approved-via-status` → гейт пропускает, БЕЗ повторного запуска `check_analysis_approved`.
- **Dockerfile НЕ копирует `scripts/`** в образ — `staging_check.py` доступен в контейнере только через mount. Путь запуска внутри контейнера учитывать (не `/app/scripts`).
- **Перезапуск стадии вручную:** `update_task_stage(task_id, "<stage>")` + `enqueue_job(agent, repo, task_content, task_id)`. Guard перед этим: `agent_running IS NULL` И нет jobs со `status IN ('queued','running')` для task_id.
---
## 6. Итог по гейтам/ядру после серии ORCH-45/46/47/48
-`check_ci_green` — поллинг (ORCH-45)
-`check_tests_passed` — читает `result:` (ORCH-47)
-`stage_engine` — передаёт деву **текст** findings, не только ссылку (ORCH-46)
- ✅ B6 staging — читает реестр ВНУТРИ staging-контейнера, больше не ложный FAIL (ORCH-48) → **deploy-staging разблокирован для всех ORCH-задач**
Конвейер стал по-настоящему автономным: задача проходит analyst→deploy без ручного пинания стадий.

View File

@@ -1,124 +0,0 @@
# Orchestrator Deploy Hook
`scripts/orchestrator-deploy-hook.sh` — хост-скрипт деплоя orchestrator с health-чеком и авто-rollback.
## Как работает
### Режим `--deploy` (по умолчанию)
1. **Захват текущего образа** — до рестарта записывает ID образа работающего контейнера в `$PREV_IMAGE_FILE` (best-effort, не падает если сервис не запущен).
2. **git pull** — обновляет код репозитория.
2b. **Build-once retag** (ORCH-036, BR-6) — если задан `$SOURCE_IMAGE`, хук ретегает его на `$TARGET_IMAGE` (`docker tag $SOURCE_IMAGE $TARGET_IMAGE`) и поднимает контейнер на этом образе через `up -d --no-build`. Это деплой РОВНО того образа, что прошёл staging, **без `docker build`**. Если `$SOURCE_IMAGE` не задан (дефолт) — шаг пропускается (обратная совместимость).
- **Fail-closed провенанс-guard** (ORCH-058, Strategy B) — ПЕРЕД `docker tag`, если задан `$EXPECTED_REVISION`, хук сверяет OCI-лейбл `org.opencontainers.image.revision` у `$SOURCE_IMAGE` с `$EXPECTED_REVISION`. Несовпадение / пустой лейбл (`<no value>`) / ошибка inspect → лог + `exit 1` (FAILED → авто-rollback), **прод не трогается**. Не задан `$EXPECTED_REVISION` (дефолт) → проверка пропускается (обратная совместимость для не-self репозиториев).
3. **Рестарт контейнера**`docker compose --profile $COMPOSE_PROFILE up -d --no-build $TARGET_SERVICE`.
4. **Health-цикл** — 10 попыток × 6с = до 60с. Критерий: HTTP 200 + тело содержит `"status":"ok"`.
- **Успех** → `exit 0`, лог "Deploy SUCCESS".
- **Провал** → авто-rollback (шаг 5).
5. **Авто-rollback** — восстанавливает образ из `$PREV_IMAGE_FILE`, рестарт, повторный health 5×3с.
- Если восстановился → `exit 1` (деплой провалился, откат успешен).
- Если и откат не помог → `exit 2` (критично).
### Режим `--build-staging` (ORCH-058, Strategy A)
Пересобирает **staging-образ** из провалидированного коммита и пересоздаёт 8501, чтобы артефакт, который мы валидируем, был РОВНО тем, что позже build-once ретегается в прод (инвариант `INV-FRESH`). Собирает/пересоздаёт **только staging (8501)** — никогда прод (8500).
1. `docker build --build-arg GIT_SHA=$GIT_SHA -t $TARGET_IMAGE $BUILD_CONTEXT` — пересборка из host-worktree валидированного коммита; `GIT_SHA` штампуется в OCI-лейбл `org.opencontainers.image.revision`.
2. `docker compose [--profile $COMPOSE_PROFILE] up -d --no-build $TARGET_SERVICE` — пересоздание staging на свежем образе.
3. Health-цикл 10×6с. Провал сборки/health → `exit 1`.
4. **`staging_check` против СВЕЖЕГО образа** (Strategy A, шаг 3 — ADR-001, AC-4) — после health хук запускает `docker exec $STAGING_CONTAINER python3 $STAGING_CHECK_PATH --base-url http://localhost:$TARGET_PORT --mode $STAGING_CHECK_MODE` (дефолт `--mode stub`, без LLM-трат). Запуск **внутри** staging-контейнера канонический (ORCH-048): suite читает реестр из собственного env контейнера, а `staging_check.py` берётся из bind-mount (`/repos/orchestrator/scripts/...`, не из образа). Это ровно тот артефакт, что позже build-once ретегается в прод → валидируем то, что промоутим (AC-4). PASS → `exit 0`; любой не-ноль (FAIL чека или safety-abort `ORCH_STAGING≠true`) → `exit 1`.
Запускается оркестратором на ребре `deploy-staging → deploy` (QG-под-чек `check_staging_image_fresh``rebuild_staging_image` пробрасывает явный staging-таргет, см. `INFRA.md`). Тот же контракт кодов выхода (0 = здоров **и** staging_check PASS).
### Режим `--rollback`
Вручную откатывает сервис на предыдущий образ из `$PREV_IMAGE_FILE`.
## Переменные окружения
| Переменная | Дефолт | Описание |
|------------------|-----------------------------------|-----------------------------------------------|
| `TARGET_SERVICE` | `orchestrator-staging` | Имя docker-compose сервиса |
| `TARGET_PORT` | `8501` | Порт health-check |
| `TARGET_IMAGE` | `orchestrator-orchestrator-staging` | Имя образа для retag при rollback |
| `COMPOSE_PROFILE`| `staging` | Docker compose profile (пусто = без профиля) |
| `PREV_IMAGE_FILE`| `$REPO/.deploy-prev-image-staging`| Файл для сохранения предыдущего образа |
| `SOURCE_IMAGE` | _(unset)_ | Build-once (ORCH-036): провалидированный образ для retag на `$TARGET_IMAGE` перед рестартом (без rebuild). Не задан → шаг пропущен. |
| `EXPECTED_REVISION` | _(unset)_ | Build-once (ORCH-058, Strategy B): ожидаемый git-SHA `$SOURCE_IMAGE` (лейбл `org.opencontainers.image.revision`). Задан → fail-closed guard перед `docker tag`. Не задан → проверка пропущена. |
| `GIT_SHA` | _(unset)_ | `--build-staging` (ORCH-058, Strategy A): коммит, штампуемый в OCI-лейбл `revision` при пересборке staging-образа. |
| `BUILD_CONTEXT` | `$REPO` | `--build-staging`: docker build context (host-worktree валидированного коммита). |
| `STAGING_CONTAINER` | `$TARGET_SERVICE` (`orchestrator-staging`) | `--build-staging` (ORCH-058): контейнер, внутри которого `docker exec` запускает `staging_check`. |
| `STAGING_CHECK_PATH` | `/repos/orchestrator/scripts/staging_check.py` | `--build-staging` (ORCH-058): путь к `staging_check.py` внутри контейнера (bind-mount, не образ). |
| `STAGING_CHECK_MODE` | `stub` | `--build-staging` (ORCH-058): режим `staging_check` (`stub` — быстро, без LLM; `full-real` — дожидается аналитика). |
| `LOG` | `/var/log/orchestrator/deploy-hook.log` | Лог-файл (fallback: `$REPO/deploy-hook.log`) |
> ⚠️ **Дефолт — всегда STAGING**. Прод активируется только явным переопределением env.
## Примеры запуска
### Staging (дефолт, безопасно)
```bash
cd /home/slin/repos/orchestrator
bash scripts/orchestrator-deploy-hook.sh --deploy
# или просто:
bash scripts/orchestrator-deploy-hook.sh
```
### Прод (осознанный шаг, Этап 5)
```bash
TARGET_SERVICE=orchestrator \
TARGET_PORT=8500 \
TARGET_IMAGE=orchestrator-orchestrator \
COMPOSE_PROFILE="" \
PREV_IMAGE_FILE=/home/slin/repos/orchestrator/.deploy-prev-image-prod \
bash scripts/orchestrator-deploy-hook.sh --deploy
```
### Прод build-once (ORCH-036) — ретег staging-образа, без rebuild
Так прод-деплой запускается **автоматически** исполняемым самодеплоем (Фаза B: `ssh + setsid`, см. `INFRA.md`). Ключевое отличие — `SOURCE_IMAGE` указывает на провалидированный staging-образ, который ретегается на прод-тег:
```bash
SOURCE_IMAGE=orchestrator-orchestrator-staging \
TARGET_SERVICE=orchestrator \
TARGET_PORT=8500 \
TARGET_IMAGE=orchestrator-orchestrator \
COMPOSE_PROFILE="" \
PREV_IMAGE_FILE=/home/slin/repos/orchestrator/.deploy-prev-image-prod \
bash scripts/orchestrator-deploy-hook.sh --deploy
```
### Ручной rollback staging
```bash
bash scripts/orchestrator-deploy-hook.sh --rollback
```
## Коды выхода
| Код | Значение |
|-----|------------------------------------------------------|
| `0` | Деплой успешен, сервис здоров |
| `1` | Деплой провалился; откат выполнен (или пропущен) |
| `2` | Деплой провалился И откат тоже провалился (критично) |
## Логи
```
/var/log/orchestrator/deploy-hook.log
```
Каждая строка с UTC-таймстампом в формате `[2026-06-05T06:30:00Z]`.
## Разница с enduro-deploy-hook.sh
| Функция | enduro-deploy-hook.sh | orchestrator-deploy-hook.sh |
|----------------------|-----------------------|-----------------------------|
| Захват PREV_IMG | ✅ | ✅ |
| git pull | ✅ | ✅ |
| Рестарт | ✅ | ✅ |
| Health-цикл (60с) | ❌ | ✅ 10×6с |
| Авто-rollback | ❌ | ✅ |
| Параметризация (env) | ❌ хардкод | ✅ дефолт=staging |
| Compose profile | ❌ | ✅ --profile staging |

View File

@@ -1,239 +0,0 @@
# INFRA.md — инфраструктура и эксплуатация оркестратора
> RUNBOOK. Топология, контейнеры, порты, переменные окружения, границы.
> **Секреты тут НЕ хранятся** — только дескрипторы. Реальные значения — в `.env` на хосте.
## Топология
```
host: mva154 (slin@82.22.50.71), network_mode: host
┌──────────────────────────────────────────────────────────────────────┐
│ orchestrator (PROD) :8500 env_file .env │
│ БД: ./data/orchestrator.db (обслуживает ВСЕ прод-проекты) │
│ │
│ orchestrator-staging (STAGING) :8501 env_file .env.staging │
│ БД: ./data/staging/orchestrator.db (изолирована, только sandbox) │
│ profile: staging — НЕ стартует обычным `docker compose up` │
└──────────────────────────────────────────────────────────────────────┘
│ webhooks │ git
▼ ▼
Plane (ag_proj) Gitea (localhost:3000)
/repos/<project> ← общий каталог репозиториев (host: /home/slin/repos)
```
## Контейнеры
| Контейнер | Роль | Порт | env_file | БД (хост) | Старт |
|-----------|------|------|----------|-----------|-------|
| `orchestrator` | прод | 8500 | `.env` | `./data/orchestrator.db` | `docker compose up -d` |
| `orchestrator-staging` | staging / песочница | 8501 | `.env.staging` | `./data/staging/orchestrator.db` | `docker compose --profile staging up -d orchestrator-staging` |
Оба: `network_mode: host`, `init: true` (tini как PID 1 — reaping зомби, B-2), `restart: unless-stopped`.
### Рантайм-uid (ORCH-040)
Оба сервиса бегут под `user: "1000:1000"` (slin), **не** root. Артефакты конвейера
(git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) создаются как
`slin:slin`, поэтому `git pull` / `git reset` на хосте под slin работают без ручного
`chown`. Доступ к docker.sock сохранён через `group_add: ["999"]` (gid docker, **не**
через root — НЕ удалять). При переносе на другой хост uid пересматривается. См.
ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и глобальный
`docs/architecture/adr/adr-0005-container-runs-as-host-uid.md`.
**Host-prerequisites (обязательная процедура Owner, в git не коммитятся):**
- **P-1 (блокер):** uid 1000 читает claude creds — `chown -R 1000:1000 /home/slin/.claude`;
проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json`. Без этого
preflight (ORCH-044) заворачивает весь конвейер.
- **P-2:** ssh-ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000 (маунт ведёт в `/home/slin/.ssh`).
- **P-3:** `id slin``1000:1000`; `/repos`, `/app/data` уже `1000:1000`.
- **P-4:** прод-рестарт self — только в окно тишины (`GET /status` без активных задач):
общий инстанс с enduro-trails.
- **P-5 (блокер миграции uid, ORCH-057):** нормализация **всех** legacy `root:root` файлов в `/repos`
— см. подраздел «Миграция uid: обязательная нормализация legacy root-файлов» ниже. Без неё первый
job падает на launch при создании worktree (инцидент 06.06, ORCH-043).
### Миграция uid: обязательная нормализация legacy root-файлов (ORCH-057)
ORCH-040 сменил `user:` контейнера, но **не** владельца уже существующих файлов в bind-mount `/repos`,
созданных прежним root-контейнером. Под uid 1000 `src/git_worktree.py::ensure_worktree` не может
создать worktree рядом с `root:root` каталогом `/repos/_wt/``fatal: could not create leading
directories … Permission denied` (агент даже не стартует). С ORCH-057 эта ошибка распознаётся и
выдаётся **внятно** (с лечащей командой) + детектится на старте сервиса (WARNING/Telegram, блок
`fs_ownership` в `GET /queue`), но **фактический `chown` обязан выполнить оператор под root на хосте**
(контейнер бежит без root и chown'ить чужие файлы не может).
**Обязательный разовый шаг при миграции uid / на новой среде (под root на mva154, ПЕРЕД стартом app):**
```bash
# 1) worktree-корень (все ветки всех проектов режутся здесь)
sudo chown -R 1000:1000 /home/slin/repos/_wt
# 2) .git обоих репо (objects / worktrees-административные записи)
sudo chown -R 1000:1000 /home/slin/repos/orchestrator/.git \
/home/slin/repos/enduro-trails/.git
# 3) корень orchestrator целиком (включая data/runs/*.log — 37 root-логов в инциденте)
sudo chown -R 1000:1000 /home/slin/repos/orchestrator
# Проверка (пусто = ок):
find /home/slin/repos/_wt ! -uid 1000 -print -quit
```
Процедура **идемпотентна** (повтор на корректной среде — no-op) и входит в **чеклист деплоя/миграции
self**. Область охвата: `_wt`, оба `.git` (`objects`+`worktrees`), `data/runs`. См.
`docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md` и сквозной
`docs/architecture/adr/adr-0031-legacy-ownership-normalization.md`.
### Тома (volumes)
- `./data``/app/data` (БД; у staging — `./data/staging`)
- `/home/slin/repos``/repos` (рабочие репозитории проектов)
- `/var/run/docker.sock` (для docker-операций деплоя)
- claude-code, node, `~/.claude*` (CLI агентов, ro)
- `~/.orchestrator-ssh``/home/slin/.ssh` (ro, деплой по ssh; target в HOME агента,
согласован с `HOME=/home/slin` из launcher — ORCH-040, ранее `/root/.ssh`)
### Disk-watchdog: мониторинг заполнения диска mva154 (ORCH-063)
07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь конвейер всех проектов**
(один прод-инстанс `orchestrator` на общей БД/очереди). Чтобы такой инцидент сигнализировался
**заранее**, работает фоновый daemon-поток `src/disk_watchdog.py` (каркас `reconciler`/`job_reaper`):
- **Что мониторится:** заполнение **хост-разделов** по смонтированным bind-путям (`/repos`
host `/home/slin/repos`, `/app/data` → host `./data`) через stdlib `shutil.disk_usage`НЕ
overlay `/` контейнера (иначе замер ложно-низкий). Пути с одним физическим устройством (`st_dev`)
дедуплицируются → один алерт, не два.
- **Порог и период:** при заполнении **≥ 85%** (`ORCH_DISK_MONITOR_THRESHOLD_PCT`) шлётся
Telegram-алерт оператору; замер — раз в 300с (`ORCH_DISK_MONITOR_INTERVAL_S`). Пока диск выше
порога, повтор — не чаще раза в ~6ч (`ORCH_DISK_MONITOR_REALERT_S`, анти-спам). При возврате
ниже порога — однократное recovery-сообщение.
- **Как отключить:** `ORCH_DISK_MONITOR_ENABLED=false` (демон не стартует; `GET /queue`
`disk_monitor.enabled=false`; поведение 1:1 как сейчас). Наблюдаемость — блок `disk_monitor` в
`GET /queue` (последний замер: `used_pct`/`free_gb`/`alerting`/`last_alert_at` по каждому пути).
- **Что делать при алерте:** watchdog **только сигнализирует** — он не трогает диск/контейнер и не
рестартит прод (self-hosting безопасность). Освобождение **docker build cache** автоматизировано
отдельным демоном (ORCH-062, см. ниже); прочие «пожиратели» — старые worktree-каталоги
`/home/slin/repos/_wt/*` завершённых задач, логи, dangling-образы (`docker image prune`) —
по-прежнему **ручная** операция оператора (авто-уборка этих категорий — вне объёма ORCH-062/063).
### Build-cache-pruner: авто-prune docker build cache на mva154 (ORCH-062)
Доминирующий «пожиратель» в инциденте 07.06.2026 — **docker build cache** (≈11 ГБ от частых
пересборок прод/staging-образов). Чтобы он не мог снова заполнить диск **без оператора**, работает
фоновый daemon-поток `src/build_cache_pruner.py` (каркас `disk_watchdog`) — «вторая половина»
watchdog'а: **watchdog сигналит, pruner убирает**.
- **Что делает:** каждые `ORCH_BUILD_CACHE_PRUNE_INTERVAL_S` (дефолт 21600с = 6ч) выполняет
**строго `docker builder prune -f --filter until=<until>`** (BuildKit GC; дефолт `until=24h`
удаляется build cache старше суток, тёплый свежий кэш сохраняется). Команда затрагивает **только
build cache** — НЕ образы/контейнеры запущенных сервисов; рестарт docker daemon/прода НЕ
выполняется (self-hosting безопасность).
- **Как исполняется:** в контейнере нет `docker` CLI (образ несёт только `openssh-client git`),
поэтому уборка идёт **на хосте через ssh** тем же каналом `ORCH_DEPLOY_SSH_USER@_HOST`, что
деплой/`image_freshness`. **Пустой `ORCH_DEPLOY_SSH_HOST` → тик no-op** (фича активна только на
self-host, где ssh настроен).
- **Как отключить:** `ORCH_BUILD_CACHE_PRUNE_ENABLED=false` (демон не стартует; поведение 1:1 как
до ORCH-062). Наблюдаемость — блок `build_cache_prune` в `GET /queue` (`enabled`/`interval_s`/
`until`/`last_run_ts`/`last_reclaimed`/`last_error`); never-raise; in-memory учёт (без миграции).
- **Ручной fallback** (если ssh-канал недоступен) — host-cron на mva154:
`0 */6 * * * docker builder prune -f --filter until=24h` (off-git, процедура Owner).
## Переменные окружения (карта; значения — в `.env`)
| Переменная | Назначение |
|-----------|-----------|
| `ORCH_PLANE_API_URL` / `_TOKEN` / `_WORKSPACE_SLUG` | доступ к Plane API |
| `ORCH_PLANE_WEB_URL` | внешний (браузерный) web-URL Plane для кликабельных ссылок на issue в уведомлениях (ORCH-017); пусто → фолбэк на `ORCH_PLANE_API_URL`, loopback-фолбэк → ссылка опускается |
| `ORCH_PLANE_WEBHOOK_SECRET` | HMAC-проверка вебхуков Plane |
| `ORCH_GITEA_URL` / `_TOKEN` / `_WEBHOOK_SECRET` | доступ к Gitea + HMAC |
| `ORCH_CLAUDE_BIN` | путь к claude CLI |
| `ORCH_REPOS_DIR` / `ORCH_HOST_REPOS_DIR` | каталог репозиториев (в контейнере / на хосте) |
| `ORCH_DB_PATH` | путь к SQLite БД |
| `ORCH_PROJECTS_JSON` | реестр проектов (Plane id → repo + prefix); пусто → дефолт из `src/projects.py` |
| `ORCH_AGENT_MODEL_DEFAULT` | LLM-модель агентов по умолчанию (ORCH-41); дефолт `claude-opus-4-8` |
| `ORCH_AGENT_MODEL_<AGENT>` | per-agent модель (ANALYST/ARCHITECT/DEVELOPER/REVIEWER/TESTER/DEPLOYER); пусто → default |
| `ORCH_AGENT_EFFORT_DEFAULT` | режим работы `--effort` по умолчанию (ORCH-41): low\|medium\|high\|xhigh\|max; дефолт `high` |
| `ORCH_AGENT_EFFORT_<AGENT>` | per-agent effort; дефолт: думающие → high, tester/deployer → medium |
| `ORCH_AGENT_FALLBACK_MODEL` | опц. фолбэк-модель при overloaded (`--fallback-model`); пусто → без флага |
| `ORCH_SELF_DEPLOY_ENABLED` | ORCH-036 kill-switch исполняемого самодеплоя (true); false → legacy-путь для всех |
| `ORCH_SELF_DEPLOY_REPOS` | CSV репозиториев с реальным самодеплоем; пусто → только self-hosting `orchestrator` |
| `ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE` | требовать человеческий Plane «Approved» для прод-деплоя (true, безопасно) |
| `ORCH_DEPLOY_FINALIZE_DELAY_S` / `_MAX_ATTEMPTS` | задержка и бюджет defer'ов finalizer'а (Фаза C; 90 / 10) |
| `ORCH_DEPLOY_SSH_USER` / `_SSH_HOST` | куда запускается detached хост-деплой (Фаза B, `ssh user@host`) |
| `ORCH_DEPLOY_HOOK_SCRIPT` / `_HOST_REPO_PATH` | путь к хук-скрипту (отн. репо) и чекаут orchestrator на хосте |
| `ORCH_DEPLOY_PROD_SOURCE_IMAGE` | staging-образ для build-once retag на прод-тег (без rebuild) |
| `ORCH_DEPLOY_PROD_TARGET_SERVICE` / `_TARGET_PORT` / `_TARGET_IMAGE` / `_COMPOSE_PROFILE` / `_PREV_IMAGE_FILE` | прод-цель хука + снапшот для авто-rollback |
| `ORCH_IMAGE_FRESHNESS_ENABLED` | ORCH-058 единый kill-switch провенанса staging-образа (A+B как целое); дефолт `true`, false → legacy build-once без проверки свежести |
| `ORCH_IMAGE_FRESHNESS_REPOS` | CSV репозиториев с реальным гейтом свежести; пусто → только self-hosting `orchestrator` |
| `ORCH_RECONCILE_ENABLED` | kill-switch sweeper потерянных webhook (ORCH-053); дефолт `true`. **При инциденте/раскатке**`false` глушит весь фоновый reconciler |
| `ORCH_RECONCILE_PLANE_ENABLED` | отдельный флаг F-2 (опрос Plane API); `false` гасит только plane-ветку, F-1 продолжает работать; дефолт `true` |
| `ORCH_RECONCILE_INTERVAL_S` | период фонового прохода reconciler, сек; дефолт `120` |
| `ORCH_RECONCILE_GRACE_DEFAULT_S` | порог «застряла» по `tasks.updated_at`, сек; дефолт `600` |
| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | per-stage пороги, напр. `{"development":300}`; невалидный JSON → дефолт |
| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | слать Telegram при разблокировке застрявшей задачи; дефолт `true` |
| `ORCH_DISK_MONITOR_ENABLED` | kill-switch disk-watchdog (ORCH-063); дефолт `true`. `false` → демон не стартует, поведение 1:1 как сейчас |
| `ORCH_DISK_MONITOR_INTERVAL_S` | период heartbeat-замера заполнения диска, сек; дефолт `300` |
| `ORCH_DISK_MONITOR_THRESHOLD_PCT` | порог заполнения для алерта, %; дефолт `85` (валидация 1..100, иначе → дефолт) |
| `ORCH_DISK_MONITOR_REALERT_S` | cooldown повторного алерта, пока выше порога, сек; дефолт `21600` (~6 ч) |
| `ORCH_DISK_MONITOR_PATHS` | CSV отслеживаемых **хост**-bind-путей; пусто → `/repos,/app/data` |
| `ORCH_BUILD_CACHE_PRUNE_ENABLED` | kill-switch build-cache-pruner (ORCH-062); дефолт `true`. `false` → демон не стартует, поведение 1:1 как до задачи |
| `ORCH_BUILD_CACHE_PRUNE_INTERVAL_S` | период тика авто-prune, сек; дефолт `21600` (~6 ч); валидация >0, иначе → дефолт |
| `ORCH_BUILD_CACHE_PRUNE_UNTIL` | возраст удержания тёплого кэша (`docker builder prune --filter until=`); дефолт `24h`; валидация `^\d+[smhdw]?$`, иначе → `24h` |
| `ORCH_BUILD_CACHE_PRUNE_ALL` | добавить `-a` к prune (только в паре с `until`); дефолт `false` |
| `ORCH_BUILD_CACHE_PRUNE_TIMEOUT_S` | таймаут ssh-команды prune, сек; дефолт `120` |
| `ORCH_BUILD_CACHE_PRUNE_NOTIFY_MIN_GB` | Telegram при освобождении ≥ N ГБ; дефолт `0` (тихо) |
| `DEPLOY_SSH_USER` / `_HOST` / `DEPLOY_HOOK_SCRIPT` | параметры деплой-хука |
**Секреты — только в `.env` / `.env.staging` на хосте, в гит НЕ коммитятся.** Канон — `.env.example`, `.env.staging.example`.
## Реестр проектов (`src/projects.py`, ORCH-6)
Связывает Plane project id → gitea repo + work-item prefix. Источник: `ORCH_PROJECTS_JSON`, fallback — встроенный дефолт. Прод видит: `enduro-trails` (ET), `orchestrator` (ORCH). Staging видит ТОЛЬКО `orchestrator-sandbox` (SANDBOX) — изоляция.
## Модель и effort агентов (`src/config.py` + `src/agents/launcher.py`, ORCH-41)
Модель LLM и режим работы (`--effort`) каждого агента **конфигурируемы** — глобально per-agent (env) и per-project (через `ORCH_PROJECTS_JSON`).
**Приоритет резолвинга** (`resolve_agent_model` / `resolve_agent_effort`):
1. per-project override — `agent_models` / `agent_efforts` в записи `ORCH_PROJECTS_JSON`;
2. per-agent env — `ORCH_AGENT_MODEL_<AGENT>` / `ORCH_AGENT_EFFORT_<AGENT>` (если непусто);
3. глобальный дефолт — `ORCH_AGENT_MODEL_DEFAULT` (`claude-opus-4-8`) / `ORCH_AGENT_EFFORT_DEFAULT` (`high`);
4. пусто → флаг не передаётся, действует дефолт CLI.
**Значения effort:** `low` < `medium` < `high` < `xhigh` < `max` — рычаг «качество vs стоимость/время». Дефолтная раскладка: думающие агенты (analyst/architect/developer/reviewer) → `high`, механические (tester/deployer) → `medium`. Невалидное значение → лог-warning, флаг опускается.
**Per-project override в `ORCH_PROJECTS_JSON`** (поля `agent_models` / `agent_efforts` опциональны, старые записи работают):
```json
{"plane_project_id":"...","repo":"orchestrator","work_item_prefix":"ORCH",
"agent_models":{"developer":"claude-opus-4-8","reviewer":"claude-sonnet-4-6"},
"agent_efforts":{"developer":"xhigh","tester":"low"}}
```
> ⚠️ Бюджет (ORCH-38): `claude-opus-4-8` дефолт в коде; реальное переключение прод-env делается отдельно после согласования.
## ⚠️ Self-hosting — оркестратор дорабатывает САМ СЕБЯ
**Факт:** прод-инстанс `orchestrator` (8500) — ОДИН на ВСЕ прод-проекты (enduro-trails + orchestrator), с ОБЩЕЙ БД `./data/orchestrator.db` и общей очередью задач (ORCH-1).
**Следствие — групповой риск:** когда орк выполняет задачу из проекта ORCH (дорабатывает себя), он бежит в том же инстансе, что обслуживает enduro-trails.
- Рестарт / падение прод-контейнера орк-задачей → конвейер ВСЕХ проектов встаёт.
- Кривой self-деплой (ORCH-36, Вариант B) → лежат все проекты сразу.
- Общая очередь → орк-задача занимает concurrency-слоты других проектов.
**Что изолировано (безопасно):**
- Staging (8501) — отдельная БД (`./data/staging`), отдельный реестр (`ORCH_PROJECTS_JSON` = только sandbox). Прод-проекты не видит.
- Репозитории разделены, изоляция веток через git worktree (ORCH-2).
**Страховки:**
- Стадия `deploy-staging` (порт 8501) — обязательный гейт перед прод-деплоем орка. Прод-деплой недостижим, пока staging-гейт не зелёный (см. `STAGING.md`, ORCH-35). Гейт условный: реален только для self-hosting (repo=orchestrator), для остальных проектов — no-op.
- **Свежесть staging-образа (ORCH-058):** на ребре `deploy-staging → deploy` (ПОСЛЕ merge-gate, ДО Phase A) QG-под-чек `check_staging_image_fresh` пересобирает staging-образ из валидированного коммита и пересоздаёт 8501 (Strategy A), а хук перед build-once retag fail-closed сверяет OCI-лейбл `revision` с `EXPECTED_REVISION` (Strategy B). Гарантирует: в прод промоутится РОВНО провалидированный артефакт (инцидент LESSONS_ORCH-036 п.4 — тихий промоут устаревшего образа). Сборки/recreate — ТОЛЬКО staging (8501); FAIL → откат на `development`. Условный: реален только для self-hosting.
**Правила для агентов при задачах ORCH:**
1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи.
2. Все проверки деплоя — на staging (8501), боевой 8500 не трогать.
3. Деплой self — только через хук с health-check + авто-rollback (`DEPLOY_HOOK.md`).
## Эксплуатация (быстрые команды)
```bash
# статус
docker ps --filter name=orchestrator
curl -s http://localhost:8500/health
curl -s http://localhost:8500/status # активные задачи
curl -s http://localhost:8500/queue # очередь
# поднять staging-песочницу
docker compose --profile staging up -d orchestrator-staging
curl -s http://localhost:8501/health
# логи
docker logs --tail 100 orchestrator
```
---
*RUNBOOK 2026-06-05. Обновлять при изменении топологии/портов/переменных. См. CONTRIBUTING.md §8.*

View File

@@ -1,200 +0,0 @@
# ONBOARDING — turnkey-онбординг нового проекта (ORCH-009)
> RUNBOOK. Полный чеклист подключения нового проекта к оркестратору одним проходом.
> Исполнитель — оператор; инструмент — CLI `scripts/onboard_project.py`
> (режимы `plan` — дефолт/dry-run, `apply`, `verify`). Каждый шаг, который CLI выполнить
> не может, помечен **🖐 РУЧНОЙ ШАГ** и снабжён командой проверки результата.
> Архитектура решения — см. «Ссылки» внизу.
Запуск CLI — из корня чекаута репо orchestrator, в venv с `requirements.txt`:
```bash
python3 scripts/onboard_project.py plan \
--name "My Project" --description "зачем проект" \
--repo my-project --prefix MP \
--stack "Python 3.12 + FastAPI" --test-cmd "pytest tests/ -q" \
--prod-port 8600 --staging-port 8601 \
--webhook-url https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea
```
`plan` печатает полный план **без единой мутации** (ни сети-POST, ни записи на диск);
`apply` — идемпотентный ensure (существующее → `skipped(exists)`, ничего не удаляется);
exit-коды: `0` — чисто, `2` — есть `manual-step`/gap, `1` — ошибка.
---
## 0. Предусловия
Все значения — в `.env` на хосте (секреты в гит не попадают):
| Переменная | Зачем | Проверка |
|-----------|-------|----------|
| `ORCH_PLANE_API_TOKEN` (+`ORCH_PLANE_API_URL`, `ORCH_PLANE_WORKSPACE_SLUG`) | создание/чтение проекта, статусов, лейблов | `curl -s -H "X-API-Key: $TOKEN" $URL/api/v1/workspaces/$SLUG/projects/ \| head -c 200` |
| `ORCH_GITEA_TOKEN` (+`ORCH_GITEA_URL`) | создание репо + webhook | `curl -s -H "Authorization: token $TOKEN" $URL/api/v1/user \| head -c 200` |
| `ORCH_GITEA_WEBHOOK_SECRET` | HMAC webhook (переиспользуется, один на все репо) | есть строка в `.env`; нет → `apply` сгенерирует и выведет |
| `ORCH_PROJECTS_JSON` | текущий реестр — источник merged-вывода | `grep ORCH_PROJECTS_JSON .env` |
Токен Plane должен иметь право создавать проекты в workspace; токен Gitea — создавать репо и
hooks под выбранным owner (`--gitea-owner`, дефолт из конфига).
---
## 1. Слой Plane: проект + статусы + лейблы
Выполняет `apply` (или вручную при недоступности API CE — каждый отказ CLI помечает
`manual-step`, не падает).
1. **Проект**: создаётся с `identifier = --prefix`. Уже существует → передай
`--plane-project-id <uuid>` (ensure распознает и пропустит).
2. **Статусы — точные канонические имена** (22, источник — `plane_sync._PLANE_NAME_TO_KEY`;
опечатка = тихая деградация fail-closed веток):
| Статус | Группа | | Статус | Группа |
|--------|--------|-|--------|--------|
| Backlog | `backlog` | | In Review | `started` |
| Todo | `unstarted` | | Blocked | `started` |
| To Analyse | `unstarted` | | Approved | `started` |
| In Progress | `started` | | Rejected | `started` |
| Analysis | `started` | | **Confirm Deploy** | `started` |
| Architecture | `started` | | Needs Input | `started` |
| Development | `started` | | Done | `completed` |
| Code-Review | `started` | | Cancelled | `cancelled` |
| Review | `started` | | **STOP** | **`cancelled`** |
| Testing | `started` | | Awaiting Deploy | `started` |
| Deploying | `started` | | Monitoring after Deploy | `started` |
⚠️ Код-критично: `STOP` обязан быть в группе `cancelled` (иначе ветка отмены молча не
активируется); в терминальных группах (`completed`/`cancelled`) — ТОЛЬКО
Done/Cancelled/STOP, иначе terminal-detection ложно сочтёт живую задачу терминальной.
3. **Лейблы**: `autoApprove`, `autoDeploy`, `Bug` (имена — из конфига оркестратора; их
отсутствие = fail-safe ручной режим / полный цикл).
4. **🖐 РУЧНОЙ ШАГ — порядок статусов на доске**: drag-and-drop в UI (API не управляет
порядком). Проверка: открой доску проекта — колонки в порядке конвейера.
5. **Workspace-webhook**: уже **существует** (один на весь workspace, создан на уровне
workspace заранее) — CLI его НЕ создаёт, только напоминает проверить:
```bash
docker exec -e PGPASSWORD=plane plane-app-plane-db-1 psql -U plane -d plane -c \
"SELECT id, url, is_active FROM webhooks;"
```
---
## 2. Слой Gitea: репо + per-repo webhook
1. **Репо** `--gitea-owner/--repo`: создаётся пустым (`auto_init=false`; ветку `main` создаст
initial push следующего слоя). Существует → `skipped(exists)`.
2. **Per-repo webhook**: `events: push/pull_request/status`, `content_type: json`,
`branch_filter: *`, URL = `--webhook-url`. **Секрет переиспользуется** из
`ORCH_GITEA_WEBHOOK_SECRET` (приёмник валидирует ОДИН глобальный секрет на все репо;
новый секрет сломал бы HMAC существующих вебхуков). Секрета нет в env → CLI сгенерирует и
выведет строку для `.env`**🖐 РУЧНОЙ ШАГ**: добавить её в `.env` (в гит не коммитить).
Формат и проверка — `docs/operations/SETUP_WEBHOOKS.md`. Проверка:
```bash
curl -s -H "Authorization: token $ORCH_GITEA_TOKEN" \
"$ORCH_GITEA_URL/api/v1/repos/<owner>/<repo>/hooks" | python3 -m json.tool
```
3. **Branch protection `main` НЕ включать** (ADR D10): required-approvals/status-checks ломают
PR-merge API merge-актора конвейера (ложные HOLD). Защита держится конвенцией + скоупом
токенов.
---
## 3. Слой kit: материализация + initial push
1. `apply` рендерит kit (`onboarding/repo-skeleton/`, плейсхолдеры `{{NAME}}` из
`onboarding/placeholders.json`) во временный каталог, докладывает live-copy канона
(`docs/_templates/` 16 скелетов + `docs/_standards/` 3 стандарта — verbatim из текущего
чекаута, BR-2 «канон не форкается») и пушит **ТОЛЬКО в свежесозданный/пустой репо**
(единственный разрешённый push; коммит `feat: onboarding skeleton (ORCH-009 kit)`).
2. Репо непустой → шаг помечается `manual-step`: **🖐 РУЧНОЙ ШАГ** — занеси недостающие
файлы обычным PR с ревью; поверх существующего контента ничего не пушится (BR-9).
3. После рендера не должно остаться ни одного `{{...}}`: CLI падает на этом сам; повторная
проверка — `verify` (скан плейсхолдеров в файлах репо).
---
## 4. Регистрация в реестре оркестратора
> ⚠️ **САМЫЙ ВАЖНЫЙ РУЧНОЙ СЛОЙ.** CLI `.env` прода НЕ правит и контейнер НЕ рестартит
> (инвариант NFR-2) — он только печатает готовую строку.
1. **🖐 РУЧНОЙ ШАГ — env**: возьми из отчёта `apply` строку
`ORCH_PROJECTS_JSON=[...полный merged-массив...]` (существующие записи verbatim + новая в
конец; строка уже провалидирована фактическим парсером реестра) и замени ею строку в `.env`
оркестратора на хосте. Вставляется атомарно одной строкой — ручное слияние JSON не нужно.
2. **🖐 РУЧНОЙ ШАГ — управляемый рестарт оркестратора**: реестр строится при импорте, нужна
перезагрузка процесса. **Self-hosting предупреждение: прод-контейнер один на ВСЕ проекты —
рестарт = групповое окно** (встаёт конвейер всех проектов). Выполняй осознанно: дождись
тихого окна (`GET /queue` — нет бегущих job), затем штатный рестарт по
`docs/operations/INFRA.md`. Проверка после рестарта:
```bash
curl -s http://localhost:8500/health
curl -s http://localhost:8500/queue | python3 -m json.tool | head -30 # реестр жив, конвейер пуст/цел
```
3. TTL-self-heal статусов Plane (300с) рестарта НЕ требует: статусы/лейблы, созданные после
регистрации, подхватятся сами.
---
## 5. Верификация
1. **`verify`-режим CLI** (read-only):
```bash
python3 scripts/onboard_project.py verify --name ... --repo ... --prefix ... \
--plane-project-id <uuid> --stack ... --test-cmd ... --prod-port ... --staging-port ... \
--webhook-url https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea
```
Проверяет: запись реестра парсится и совпадает по полям; все 22 статуса резолвятся
(включая fail-closed `Confirm Deploy`/`STOP`); лейблы на месте; webhook существует и
активен; kit-файлы в репо (6 промптов, `AGENTS.md`, `INFRA.md`, `_templates`/`_standards`);
нет неразрешённых плейсхолдеров. Любой gap → exit `2` с перечнем.
2. **Smoke на песочнице (ADR D8)** — контур: **staging-оркестратор (порт 8501, изолированная
БД `./data/staging`)** + одноразовый sandbox-проект (рекомендуемые имена: проект
`onboarding-smoke`, префикс `SMK`, репо `onboarding-smoke`):
1. Онборди sandbox самим CLI (слои 13 этого runbook).
2. **🖐 РУЧНОЙ ШАГ**: зарегистрируй sandbox в `ORCH_PROJECTS_JSON` **`.env.staging`**
(не прода!) и перезапусти staging-контейнер (он свободен от прод-инварианта):
`docker compose --profile staging up -d orchestrator-staging`.
3. Создай тестовую задачу в sandbox-проекте → доведи до стадии analysis в песочнице.
4. Критерий PASS: агент по своему промпту **прочитал доку проекта** (следы чтения
`CLAUDE.md`/`AGENTS.md` в выводе) и **записал артефакты** в `docs/work-items/SMK-…/`
по канону `PIPELINE_DOCS.md`.
5. Запротоколируй прогон в «Журнале smoke-прогонов» (ниже). Для приёмки ORCH-009 первый
протокол обязателен.
---
## 6. Откат
CLI ничего не удаляет (BR-9) — откат всегда ручной и осознанный:
| Что создано | Как откатить | Проверка |
|-------------|--------------|----------|
| Plane-проект (+статусы/лейблы) | удалить проект в UI Plane | проект исчез из списка workspace |
| Gitea-репо (+webhook) | удалить репо в UI/API Gitea (webhook умрёт вместе с ним) | `GET /api/v1/repos/<owner>/<repo>` → 404 |
| Строка реестра | убрать запись из `ORCH_PROJECTS_JSON` в `.env` + управляемый рестарт (см. слой 4, то же групповое окно) | `GET /queue` — проекта нет в реестре |
| Sandbox-артефакты smoke | удалить sandbox-проект/репо после прогона (или архивировать) | см. выше |
---
## Журнал smoke-прогонов
| Дата | Оператор | Параметры (проект/префикс/репо) | Контур | Результат (PASS/FAIL) | Протокол |
|------|----------|----------------------------------|--------|------------------------|----------|
| — | — | — (первый прогон фиксируется при приёмке ORCH-009) | staging 8501 | — | — |
---
## Ссылки
- Архитектура решения: `docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md`
(D1…D11); сквозной ADR — `docs/architecture/adr/adr-0035-turnkey-project-onboarding.md`.
- Устройство набора шаблонов и словарь плейсхолдеров: `onboarding/README.md`.
- Формат вебхуков: `docs/operations/SETUP_WEBHOOKS.md`; топология и рестарты —
`docs/operations/INFRA.md`.

View File

@@ -1,125 +0,0 @@
# 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`.

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