Compare commits

...

163 Commits

Author SHA1 Message Date
5bb0d6f88c developer(ET): auto-commit from developer run_id=803
All checks were successful
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m21s
2026-06-17 22:35:10 +03:00
9b5f06a477 test(estimator): cover never-raise/edge branches to restore coverage gate (ORCH-020)
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m22s
Coverage-gate failed (measured 79.48% < baseline 80.45%−ε) because the new
ORCH-020 estimator code added ~1500 lines but the happy-path TC-01..TC-20 suite
left the leaf's never-raise/fail-safe arms uncovered.

Adds tests/test_orch020_estimator_branches.py (46 cases) targeting the untested
branches of the new code only — no src/ change:
  * estimator.py: applies/should_estimate error arms, _sp_thresholds malformed +
    non-ascending fall-through, story_points_for bad input, _forecast history
    (full-samples + bootstrap blend), _resolve_task_and_track (repo-from-task +
    never-raise), estimate()/record_actual_on_done/_task_wall_seconds/_comment_text/
    card_line/snapshot except arms (74% -> 99.5%);
  * db.completed_task_stats history/zero-wall arms;
  * plane_sync estimate-system writes (non-int, guard-block, estimate-points parse
    via mocked httpx, no-estimate-system, network error);
  * main.py optional /estimate POST+GET endpoints;
  * webhooks.plane.handle_estimate degenerate paths (no id, seq-derive fallback).

src/ line coverage 79.48% -> 80.68% (>= baseline 80.45%); full suite green (2294).

Refs: ORCH-020
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:34:35 +03:00
staging-runner
5c160f7de3 staging(ORCH-115): staging gate SUCCESS for ORCH-020
All checks were successful
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m34s
2026-06-17 22:15:35 +03:00
test-runner
14d479e1e8 test(ORCH-116): test gate PASS for ORCH-020
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m33s
2026-06-17 22:13:58 +03:00
d73e208d19 reviewer(ET): auto-commit from reviewer run_id=802
All checks were successful
CI / test (push) Successful in 1m31s
CI / test (pull_request) Successful in 1m24s
2026-06-17 22:11:58 +03:00
375bd468dd docs(overview): отразить операторскую способность «Оценка» в витрине (ORCH-020)
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m22s
Закрывает P1 из 12-review.md (attempt 2/3): бизнес-витрина docs/overview/
не отражала новый операторский Plane-статус «Оценка» (ORCH-020).

- business.md: бизнес-способность «Оценка задачи до запуска» + Сценарий 7.
- presentation.md: статус-жест «Оценка» на слайдах 8/9/15.
- tech-pipeline.md: новая секция «Оценка задачи: статус «Оценка»» +
  переформулировано устаревшее «управляющих статусов ровно три» в семейство
  операторских статусов-действий (запуск / гейты / STOP / Оценка).
- tech-integrations.md: то же переформулирование + запись прогноза/факта в Plane.
- tech-data-model.md: таблица task_estimates в списке вспомогательных.
- tech-observability.md: блок estimator в GET /queue + пункт «Оценка» карточки.

Также (P3): ужесточён тавтологичный assert TC-20 (был `... or True`) —
теперь проверяет, что ни один таргет ребра STAGE_TRANSITIONS не равен estimate.

Машинные факты витрины (tests/test_system_docs.py) и control-path не тронуты;
ruff на изменённом файле чист; полный pytest зелёный (2248 passed).

Refs: ORCH-020

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:04:39 +03:00
0fbacf47cb reviewer(ET): auto-commit from reviewer run_id=800
All checks were successful
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m24s
2026-06-17 21:56:07 +03:00
238de9ba44 feat(estimator): task estimation triggered by Plane status «Оценка» (ORCH-020)
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m22s
Add a deterministic task-estimation side-mechanism: a new operator Plane
status «Оценка» (third action-status, family STOP/Confirm Deploy) triggers a
new never-raise leaf src/estimator.py that forecasts cost/time/tokens/story
points {1,2,3,5,8} from the history of completed tasks (no LLM — ADR-001 D1),
writes the forecast to Plane (estimate_point + comment), the Telegram card and
the additive task_estimates ledger (UPSERT by work_item_id), then returns the
issue to Backlog. On task completion the fact (from usage.py) is written to
Plane `point`. Massivity is free (Plane multi-select -> N webhooks); re-estimate
is idempotent; anti-disruption skips in-flight tasks; anti-loop (Backlog matches
no trigger branch).

INVARIANT (NFR-1/NFR-3): the estimator is an observer/producer, never a Quality
Gate or stage — STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys /
existing table schemas and the hot launch path (resolve_agent_model/_spawn) are
byte-for-byte untouched. fail-closed: `estimate` absent from _DEFAULT_STATES ->
a board without the status is inert (zero regression, enduro untouched).

- config: ORCH_ESTIMATOR_* flags (kill-switch + CSV scope empty->self-hosting
  only, bootstrap defaults, story-point cost thresholds, wall cap).
- db: additive task_estimates table + record_estimate/set_actual/get_estimate/
  estimates_snapshot + read-only completed_task_stats history aggregate.
- plane_sync: «Оценка»->estimate name map (NOT in _DEFAULT_STATES); set_issue_
  backlog/set_issue_point/set_issue_estimate_point + get_project_estimate_points
  (all via ORCH-117 write-guard, best-effort/fail-safe).
- webhooks/plane: fail-closed estimate branch + handle_estimate (anti-disruption,
  auto-return to Backlog, off-loop).
- stage_engine: best-effort fact-on-done врезка (after terminal decision).
- notifications: «Оценка» card line (read from ledger, omitted when empty).
- main: read-only `estimator` GET /queue block + optional POST/GET /estimate.
- onboarding canon + Lite/Bundled/ONBOARDING docs: 23rd status «Оценка» (group
  unstarted, never terminal); anti-drift count pins bumped 22->23.
- tests: tests/test_orch020_estimator.py (TC-01..TC-20), full suite green.

Refs: ORCH-020
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:47:00 +03:00
6c204548a7 architect(ET): auto-commit from architect run_id=798
All checks were successful
CI / test (push) Successful in 1m14s
2026-06-17 21:16:50 +03:00
babc475ec3 analyst(ET): auto-commit from analyst run_id=797
All checks were successful
CI / test (push) Successful in 1m15s
2026-06-17 20:59:30 +03:00
f737e6730e analyst(ET): auto-commit from analyst run_id=796
All checks were successful
CI / test (push) Successful in 1m15s
2026-06-17 20:35:32 +03:00
d8ab31d200 analyst(ET): auto-commit from analyst run_id=795
All checks were successful
CI / test (push) Successful in 1m17s
2026-06-17 19:23:14 +03:00
66428edb69 docs: init ORCH-020 business request
All checks were successful
CI / test (push) Successful in 1m15s
2026-06-17 19:16:29 +03:00
e5dc4eb655 Merge pull request 'docs(operations): FAQ по статусу STOP для пользователя (ORCH-108)' (#148) from feature/ORCH-108-19c40858 into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-17 18:23:09 +03:00
deploy-finalizer
618321addb deploy(ORCH-036): finalize SUCCESS for ORCH-108
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m15s
2026-06-17 18:23:07 +03:00
staging-runner
1d552aa6aa staging(ORCH-115): staging gate SUCCESS for ORCH-108
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m16s
2026-06-17 18:02:03 +03:00
test-runner
fedd5fab73 test(ORCH-116): test gate PASS for ORCH-108
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m23s
2026-06-17 18:00:27 +03:00
21dd5a7eb0 reviewer(ET): auto-commit from reviewer run_id=794
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m17s
2026-06-17 17:58:42 +03:00
dbd8df6eb2 docs(operations): add user FAQ for STOP cancellation status (ORCH-108)
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m16s
Create docs/operations/FAQ_STOP.md — a user-facing "вопрос → ответ" FAQ for
Plane board users explaining the STOP status: what it does, how to cancel a
task, step-by-step consequences (agent stops → jobs cancelled → working branch
and worktree removed → task → cancelled → Telegram+Plane; docs artifacts
preserved, main/prod untouched), deferred cancellation in the critical
merge/deploy window, explicit "STOP does NOT revert merged/deployed code"
(revert is a separate task), restart only via "To Analyse" from scratch, no-op
causes, where to observe the result, and STOP/Approved/Confirm Deploy disambig.

docs-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, check_*, machine-verdict keys
and the DB schema are byte-for-byte untouched. STOP behaviour source of truth
remains ORCH-090 (adr-0026); the FAQ documents and links to it (link-first:
machine details markers/lease/tombstone given by reference, not duplicated).

Add two-way cross-links: docs/overview/business.md (Сценарий 6) and
docs/overview/tech-pipeline.md (Отмена: STOP → cancelled) → FAQ; FAQ → overview
+ ADR ORCH-090. Structure guarded by deterministic anti-drift test
tests/test_faq_stop_doc.py (offline, no network/LLM/subprocess; mirrors
tests/test_lite_setup_doc.py): existence + 8 section anchors + fact bricks +
cross-links + claim-level negative scan (sentence-level, not bare substrings,
so it never false-fails on correctly negating phrases — TR-3, with a
non-evergreen self-check). Full pytest tests/ green (2227 passed).

Refs: ORCH-108

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:50:58 +03:00
e97111dc74 architect(ET): auto-commit from architect run_id=792
All checks were successful
CI / test (push) Successful in 1m15s
2026-06-17 17:42:59 +03:00
d11daf11f7 analyst(ET): auto-commit from analyst run_id=791
All checks were successful
CI / test (push) Successful in 1m15s
2026-06-17 17:26:06 +03:00
56825e79fe docs: init ORCH-108 business request
All checks were successful
CI / test (push) Successful in 1m20s
2026-06-17 17:17:50 +03:00
37cbaf5b86 Merge pull request 'fix(webhooks): source-backed 00-business-request.md instead of hardcoded TBD (ORCH-119)' (#147) from feature/ORCH-119-bug-00-business-request-md-is- into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-17 17:17:48 +03:00
deploy-finalizer
43ab8938a9 deploy(ORCH-036): finalize SUCCESS for ORCH-119
All checks were successful
CI / test (push) Successful in 1m17s
2026-06-17 17:17:47 +03:00
staging-runner
02c4c4549d staging(ORCH-115): staging gate SUCCESS for ORCH-119
All checks were successful
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m15s
2026-06-17 17:11:52 +03:00
test-runner
4c9774b17d test(ORCH-116): test gate PASS for ORCH-119
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m25s
2026-06-17 17:10:16 +03:00
1b09d1a525 reviewer(ET): auto-commit from reviewer run_id=790
All checks were successful
CI / test (push) Successful in 1m27s
CI / test (pull_request) Successful in 1m19s
2026-06-17 17:08:26 +03:00
2a50391a74 developer(ET): auto-commit from developer run_id=789
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m16s
2026-06-17 15:10:50 +03:00
3d3f07ff05 docs(changelog): restore ORCH-119 entry dropped by rebase auto-merge
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m16s
The merge-gate's auto_rebase_onto_main silently dropped the ORCH-119
CHANGELOG bullet during a same-anchor 3-way merge: origin/main's
ORCH-120/126 entries were kept while the ORCH-119 insertion was lost.
Re-spliced the entry verbatim under ## [Unreleased] alongside 120/126.

Refs: ORCH-119

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:10:14 +03:00
staging-runner
24ec88c372 staging(ORCH-115): staging gate SUCCESS for ORCH-119
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m17s
2026-06-17 15:03:00 +03:00
test-runner
9819d10825 test(ORCH-116): test gate PASS for ORCH-119 2026-06-17 15:03:00 +03:00
aece77a6d9 reviewer(ET): auto-commit from reviewer run_id=788 2026-06-17 15:03:00 +03:00
test-runner
a1985f5181 test(ORCH-116): test gate FAIL for ORCH-119 2026-06-17 15:02:40 +03:00
f1e7c2aeb9 reviewer(ET): auto-commit from reviewer run_id=786 2026-06-17 15:02:40 +03:00
06cd7cb72c fix(webhooks): source-backed 00-business-request.md instead of hardcoded TBD (ORCH-119)
The Description section of 00-business-request.md always read the literal `TBD`,
losing the source-backed Plane-issue request context. Render the ACTUAL issue
`description` on both creation paths:

- Direct path A (serial_gate N/A): start_pipeline passes `description` to
  _create_initial_docs.
- Deferred path B (ORCH-088, dominates on self-hosting): persist `description`
  durable in the additive `tasks.description` column inside the same atomic INSERT
  in create_task_atomic (race-safe vs ORCH-053 anti-dup claim), read it in
  launcher._spawn -> _materialize_deferred_branch at claim (no network in the hot
  claim path, NFR-4).

Pure render helper _render_business_request with a fail-safe fallback marker for
empty/None/unreadable descriptions (never breaks task creation); Gitea 422 stays a
no-op (idempotent). STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys
and the base CREATE TABLE tasks are byte-for-byte unchanged; the ORCH-088
anti-stale-base invariant is preserved (only the data source is enriched).

Tests: tests/test_orch119_business_request.py (TC-01 mandatory red->green
regression; TC-02..TC-07). Updated the ORCH-088 serial-gate spy for the additive
_create_initial_docs arg.

Refs: ORCH-119
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:02:40 +03:00
a8b896b27f architect(ET): auto-commit from architect run_id=784 2026-06-17 15:02:40 +03:00
0f3649c5d3 analyst(ET): auto-commit from analyst run_id=783 2026-06-17 15:02:40 +03:00
b3bae0a1ca docs: init ORCH-119 business request 2026-06-17 15:02:40 +03:00
f50f61c5f5 Merge pull request 'fix(analysis): activate analyst open-questions -> Needs Input flow (ORCH-120)' (#146) from feature/ORCH-120-bug-analyst-open-questions-mus into main 2026-06-17 13:50:32 +03:00
deploy-finalizer
a1a044315b deploy(ORCH-036): finalize SUCCESS for ORCH-120
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m12s
2026-06-17 13:50:26 +03:00
staging-runner
6c95e2d689 staging(ORCH-115): staging gate SUCCESS for ORCH-120
All checks were successful
CI / test (push) Successful in 1m21s
CI / test (pull_request) Successful in 1m17s
2026-06-17 13:44:39 +03:00
test-runner
8cda3a2eb5 test(ORCH-116): test gate PASS for ORCH-120
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m26s
2026-06-17 13:43:04 +03:00
e3be810e80 reviewer(ET): auto-commit from reviewer run_id=782
All checks were successful
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m16s
2026-06-17 13:41:16 +03:00
19c31778b2 docs(overview): sync system showcase with analyst open-questions auto-park (ORCH-120)
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m14s
Address reviewer P1 (ось ORCH-011/ORCH-079, правило агентов №6): витрина
описывала паузу serial-gate как исключительно операторскую, но ORCH-120
добавил движковый авто-park/unpark на analyst Needs Input.

- tech-pipeline.md: абзац пауз теперь называет два источника (оператор +
  авто-park движком на Needs Input, флаг analyst_needs_input_autopause_enabled,
  скоуп self-hosting, симметричный unpark на resume).
- tech-observability.md: пункт пауз в GET /queue — оба источника.
- tech-agents.md: when-applicable сигнальный канал 01-questions.md у analyst
  (строка таблицы + поясняющая врезка; не machine-verdict, не deliverable).
- CHANGELOG: запись ORCH-120 дополнена строкой про обновление витрины.

tests/test_system_docs.py зелёный (29 passed). src/STAGE_TRANSITIONS/QG_CHECKS
не тронуты — docs-only.

Refs: ORCH-120
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:28:06 +03:00
452df7aedf reviewer(ET): auto-commit from reviewer run_id=780
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m14s
2026-06-17 13:24:21 +03:00
d6b495f156 fix(analysis): activate analyst open-questions -> Needs Input flow (ORCH-120)
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m11s
Activates and completes the previously dead "analyst asks BLOCKING questions ->
01-questions.md -> Needs Input" path. Four coordinated changes, additive, under
kill-switch, self-hosting scope, never-raise; STAGE_TRANSITIONS / QG_CHECKS /
check_* / machine-verdict keys / DB schema are byte-for-byte UNCHANGED (the flow
is a pre-gate engine branch, NOT a Quality Gate; 01-questions.md is a SIGNAL
artifact, NOT a machine-verdict).

- D1 contract + canon: analyst.md documents the 01-questions.md channel (blocking
  questions -> Needs Input, do NOT fabricate deliverables) + resume behaviour; new
  skeleton docs/_templates/01-questions.md; PIPELINE_DOCS.md manifest row + 01-
  prefix note.
- D2 freshness-supersede (DQ-2): pure offline mtime predicate questions_active in
  the new leaf src/analyst_questions.py (a full FRESH package supersedes a stale
  untouched 01-questions.md -> no Needs-Input loop, AC-6).
- D3 priority: questions take priority over "files ready" in
  _handle_analysis_approved_flow (_decide_analysis_outcome + _emit_analysis_*);
  off/out-of-scope runs the ORIGINAL byte-for-byte order (AC-9).
- D4 auto-park: set_task_paused on Needs Input via the ORCH-124 pause axis so the
  repo serial-gate FIFO is not wedged while waiting for a human (AC-4); D5 resume +
  unpark (clear_task_paused) in handle_status_start (analysis branch).

Flags (config.py, safe defaults): analyst_questions_gate_enabled /
analyst_questions_gate_repos (empty -> self-hosting only) /
analyst_needs_input_autopause_enabled.

Tests: test_orch120_analyst_needs_input.py (TC-01 regress + TC-02/03/06/09/10),
test_orch120_serial_gate_needs_input.py (TC-04), test_orch120_resume_unpark.py
(TC-05), test_orch120_questions_artifact_canon.py (TC-08), assert in
test_agent_prompts_canon.py (TC-07). Full suite green (2205 passed).

Refs: ORCH-120

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:15:27 +03:00
1fcbe06df5 architect(ET): auto-commit from architect run_id=778
All checks were successful
CI / test (push) Successful in 1m13s
2026-06-17 12:55:28 +03:00
432da2c4ed analyst(ET): auto-commit from analyst run_id=777
All checks were successful
CI / test (push) Successful in 1m13s
2026-06-17 12:45:52 +03:00
a4625df97c docs: init ORCH-120 business request
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-17 12:37:50 +03:00
8c74430b13 Merge pull request 'fix(queue): enforce queued ⇒ no run-ownership invariant (ORCH-126)' (#145) from feature/ORCH-126-bug-queued-job-can-keep-stale- into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-17 11:56:28 +03:00
deploy-finalizer
ab157324a7 deploy(ORCH-036): finalize SUCCESS for ORCH-126
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m13s
2026-06-17 11:56:26 +03:00
staging-runner
aca0466162 staging(ORCH-115): staging gate SUCCESS for ORCH-126
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m12s
2026-06-17 11:50:19 +03:00
test-runner
3b8aca03ee test(ORCH-116): test gate PASS for ORCH-126
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m18s
2026-06-17 11:48:44 +03:00
c8632f4b48 reviewer(ET): auto-commit from reviewer run_id=776
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m14s
2026-06-17 11:47:05 +03:00
d7e7a4d817 fix(queue): enforce queued ⇒ no run-ownership invariant (ORCH-126)
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m15s
Queued analyst-jobs hung forever even with ORCH_SERIAL_GATE_ENABLED=false
(incident ORCH-124/125, job 2286: queued + run_id=759/760 + pid=35/42 +
started_at=NULL — physically impossible). No path returning a job to
'queued' reset its run-ownership (run_id/pid); after a container restart a
reused pid made pid_alive(stale)=True, so the job-reaper Tier-1 saw a phantom
'running' and at max_concurrency=1 wedged the claim of the whole shared queue.

Enforce the invariant `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND
started_at IS NULL` on existing columns (no schema change):

- D1 forward-cleanup: requeue_running_jobs / mark_job('queued') /
  mark_job_transient / reap_running_job('queued') reset run_id=NULL, pid=NULL
  in the same UPDATE that clears started_at; atomic status-guards preserved.
- D2 clean claim: claim_next_job resets pid/run_id on the queued->running flip
  (defense-in-depth) so the row carries pid IS NULL until _spawn stamps it.
- D4 self-heal + observability: db.find_impossible_queued_jobs /
  sanitize_impossible_queued run at startup (main.lifespan) and on each reaper
  tick (JobReaper.sanitize_impossible_queued_once, never-raise); counter
  impossible_queued_total in the GET /queue reaper block. Kill-switch
  ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED (default on; gates only the D4 sweep).
- D5: reaper Tier-1 unchanged — the fix restores its precondition (pid reflects
  THIS run). Marked invariants ORCH-065/113/114/099 preserved.

Tests: tests/test_orch126_queued_stale_run.py (TC-01 mandatory regression
red->green; TC-02..TC-10). Full pytest tests/ -q green (2189 passed).
Docs: internals.md (run-ownership invariant section), .env.example, CHANGELOG;
cross-cutting adr-0052.

Refs: ORCH-126
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:39:26 +03:00
3fb7bd6e4c architect(ET): auto-commit from architect run_id=774
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-17 11:22:30 +03:00
453c5b7d04 analyst(ET): auto-commit from analyst run_id=773
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-17 11:07:33 +03:00
a5f691fc96 docs: init ORCH-126 business request
All checks were successful
CI / test (push) Successful in 1m17s
2026-06-17 11:00:16 +03:00
8e2281aab4 Merge pull request 'fix(serial-gate): pause-without-blocking via per-task park signal (ORCH-124)' (#144) from feature/ORCH-124-bug-serial-gate-treats-backlog into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-16 22:46:02 +03:00
deploy-finalizer
895fb3ab44 deploy(ORCH-036): finalize SUCCESS for ORCH-124
All checks were successful
CI / test (push) Successful in 1m13s
2026-06-16 22:46:01 +03:00
staging-runner
9709aa2267 staging(ORCH-115): staging gate SUCCESS for ORCH-124
All checks were successful
CI / test (push) Successful in 1m19s
CI / test (pull_request) Successful in 1m12s
2026-06-16 22:35:07 +03:00
test-runner
b61a4eb092 test(ORCH-116): test gate PASS for ORCH-124
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 1m16s
2026-06-16 22:33:32 +03:00
be8ddfcd57 reviewer(ET): auto-commit from reviewer run_id=772
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m13s
2026-06-16 22:31:49 +03:00
58e5dfe55d docs(serial-gate): sync system showcase + clean stray tags (ORCH-124)
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m12s
Addresses reviewer REQUEST_CHANGES (run 768) on ORCH-124 — docs-only,
no src/tests touched, fix scope unchanged.

P1: update docs/overview/ showcase for the new serial-gate "pause without
blocking" axis (changed task-routing functionality, ORCH-011/ORCH-079):
- tech-pipeline.md: FIFO exception "pause without blocking" next to freeze
- tech-data-model.md: durable signal tasks.paused_at on the Task row
- tech-observability.md: paused/reason in serial_gate GET /queue block +
  operator endpoints POST /serial-gate/pause|resume

P2: strip leaked tool-call trailing tags (</content>/</invoke>) from 4
golden-source docs of this PR (06-adr/ADR-001, adr-0051,
08-data-requirements.md, 10-tech-risks.md).

CHANGELOG "Доки" bullet extended accordingly. Full suite green (2178 passed);
test_system_docs.py green (machine-checked showcase facts intact).

Refs: ORCH-124
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 21:50:45 +03:00
ec932264db reviewer(ET): auto-commit from reviewer run_id=768
All checks were successful
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m12s
2026-06-16 20:24:55 +03:00
3a1972875f fix(tests): isolate repos_dir in ORCH-123 staging-runner test fixture
All checks were successful
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m12s
The deterministic test-runner gate (full `pytest tests/`) failed on
test_orch123_staging_runner_exec.py::test_r2_held_deploy_staging_not_rolled_back
once ORCH-124 reached the testing stage.

Root cause (pre-existing latent regress, surfaced — not introduced — by
ORCH-124): the fixture isolated `worktrees_dir` but not `repos_dir`.
`check_staging_status` falls back to `<repos_dir>/<repo>` (and its
origin/main) when the feature worktree is absent. After ORCH-123 merged,
the real `/repos/orchestrator/docs/work-items/ORCH-123/15-staging-log.md`
(verdict SUCCESS) exists on disk, so the intended-RED staging gate read it
and went green -> advance_stage was called -> the R-2 assertion failed.
Order-dependent: the test passed alone, failed in the full suite.

Fix: isolate `settings.repos_dir` to an empty tmp subdir in the fixture
(mirroring the existing worktrees_dir isolation) so the staging gate is
deterministically "not found" -> red, regardless of suite ordering. The
ORCH-123 R-2 invariant (a held deploy-staging task is never rolled back to
development, adr-0049/ADR-001 D4) is preserved and strengthened — the fix
only restores the test's stated premise. src/** / STAGE_TRANSITIONS /
QG_CHECKS / check_* untouched (test-only change).

Refs: ORCH-124
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:12:28 +03:00
test-runner
c7336dd9ea test(ORCH-116): test gate FAIL for ORCH-124
All checks were successful
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m15s
2026-06-16 19:51:06 +03:00
7ac83a9731 reviewer(ET): auto-commit from reviewer run_id=766
All checks were successful
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m15s
2026-06-16 19:49:23 +03:00
87af857082 fix(serial-gate): pause-without-blocking via per-task park signal (ORCH-124)
All checks were successful
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m17s
Fixes incident ORCH-116/ORCH-123: serial_gate defined a repo's "active task"
purely by machine stage (tasks.stage NOT IN ('done','cancelled')). Plane statuses
Backlog/Blocked/Needs-Input (layer-B indication, ORCH-066) do NOT change
tasks.stage (layer A), so a paused predecessor was indistinguishable from an active
one and held the FIFO gate closed against an urgent successor — the urgent fix
could not start until the paused task was formally done.

Introduces an explicit, durable, DB-resolvable per-task "park" signal — additive
nullable column tasks.paused_at (pattern of cancelled_at/track) — and a new
ORTHOGONAL scheduler "pause" axis. The serial-gate "active task" predicate becomes
`stage NOT IN ('done','cancelled') AND paused_at IS NULL` across all three points
(build_claim_clause / repo_has_active_task / _per_repo_snapshot). The terminal set
{done,cancelled} in serial_gate/task_deps/stages.py is byte-for-byte unchanged
(adr-0026 not regressed): task_deps/stages.py do NOT read paused_at, so a paused
declared dependency and an active repo_freeze STILL block (pause never bypasses
them — different axes). Anti-stale-base on resume relies on the existing deferred
branch cut (ORCH-088) + pre-merge auto_rebase_onto_main + merge-gate re-test
(ORCH-026/093/110) — no new rebase machinery.

Additive, under an independent sub-flag, never-raise, restart-safe; hot-claim
fail-OPEN and freeze fail-CLOSED preserved. STAGE_TRANSITIONS / QG_CHECKS / check_*
/ machine-verdict keys / existing table schemas are byte-for-byte untouched (this is
a queue-scheduler + observability change, not a Quality Gate).

- src/db.py: additive tasks.paused_at column (_ensure_column) + set/clear/is helpers
- src/serial_gate.py: _pause_layer_enabled() + pause-term in the 3 points; `paused`
  list + per-job `reason` (freeze>dependency>active-task>null) in the /queue snapshot
- src/config.py + .env.example: serial_gate_pause_enabled (default True = true no-op)
- src/main.py: POST /serial-gate/pause|resume?work_item=<id> (by образцу unfreeze)
- tests/test_orch124_serial_gate_pause.py: TC-01 mandatory incident regress + TC-02..15
- CHANGELOG.md: [Unreleased] entry

ADR: docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md
Cross-cutting: docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md

Refs: ORCH-124

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 19:35:55 +03:00
de4f067655 architect(ET): auto-commit from architect run_id=764
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-16 19:17:43 +03:00
fef5ba15d5 analyst(ET): auto-commit from analyst run_id=763
All checks were successful
CI / test (push) Successful in 1m9s
2026-06-16 17:56:23 +03:00
569abee5f2 docs: init ORCH-124 business request
All checks were successful
CI / test (push) Successful in 1m14s
2026-06-16 17:24:43 +03:00
39fe1a5081 Merge pull request 'feat(testing): deterministic test-runner replacing LLM tester on the testing stage (ORCH-116)' (#142) from feature/ORCH-116-orch-replace-llm-tester-with-d into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-16 10:27:24 +03:00
deploy-finalizer
274fbd77fc deploy(ORCH-036): finalize SUCCESS for ORCH-116
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m13s
2026-06-16 10:27:22 +03:00
staging-runner
b212afbbd0 staging(ORCH-115): staging gate SUCCESS for ORCH-116
All checks were successful
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m16s
2026-06-16 10:21:36 +03:00
3270647d86 tester(ET): auto-commit from tester run_id=758
All checks were successful
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m16s
2026-06-16 10:19:58 +03:00
e12b03b235 reviewer(ET): auto-commit from reviewer run_id=757
All checks were successful
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m23s
2026-06-16 10:11:33 +03:00
c470576202 developer(ET): auto-commit from developer run_id=756
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m11s
2026-06-16 09:59:29 +03:00
74fccf3a09 fix(testing): reconcile ORCH-116 with merged ORCH-123 (ADR renumber, CHANGELOG, env parity)
All checks were successful
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m12s
Recovery from the merge-gate rebase-conflict bounce. The feature branch was
rebased onto origin/main (which had merged ORCH-123). The single conflicting
hunk — docs/architecture/README.md — was resolved during the rebase: kept
ORCH-123's host-side staging-runner line AND the ORCH-116 test-runner bullet.

This follow-up commit reconciles the remainder:

- Renumber the global sweeping ADR adr-0049 -> adr-0050. ORCH-123 took adr-0049
  (adr-0049-host-side-docker-execution-boundary.md) on main while ORCH-116 was
  in flight, so ORCH-116 yields to the merged task and moves to the next free
  number. Mechanical cross-reference reconciliation only (git mv + title + every
  test-runner reference across README/internals/CLAUDE/CHANGELOG/config.py +
  06-adr/ADR-001 + 12-review). Main's adr-0049 host-side references are left
  byte-for-byte untouched. No design/verdict content was altered.
- Restore the ORCH-116 CHANGELOG entry that the CHANGELOG auto-merge silently
  dropped (both ORCH-123 and ORCH-116 inserted at the same [Unreleased] anchor;
  git kept only ORCH-123).
- Add the missing ORCH_TEST_RUNNER_* keys to .env.example (parity with the
  ORCH_STAGING_RUNNER_* block; ORCH-101 canon of start keys).

Refs: ORCH-116

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:56:47 +03:00
staging-runner
4b14b010de staging(ORCH-115): staging gate SUCCESS for ORCH-116 2026-06-16 09:37:40 +03:00
4c7b2345b7 reviewer(ET): auto-commit from reviewer run_id=754 2026-06-16 09:37:40 +03:00
a3ea56c751 reviewer(ET): auto-commit from reviewer run_id=743 2026-06-16 09:37:40 +03:00
staging-runner
024e1bfceb staging(ORCH-115): staging gate FAILED for ORCH-116 2026-06-16 09:37:40 +03:00
b1e00c0a7d tester(ET): auto-commit from tester run_id=742 2026-06-16 09:37:40 +03:00
e386130fd1 reviewer(ET): auto-commit from reviewer run_id=741 2026-06-16 09:37:40 +03:00
9d16ee473a feat(testing): deterministic test-runner replacing LLM tester on the testing stage (ORCH-116)
Second realised slice of the determinization-roadmap (ORCH-118 A5,
needs-hybrid-fallback): on the `testing` stage for the self-hosting
`orchestrator` repo the LLM `tester` agent is replaced by a deterministic
test-runner (src/test_runner.py), intercepted in launch_job BEFORE _spawn
(deploy-finalizer / post-deploy-monitor / staging-runner precedent).

It runs the regression `python -m pytest <target>` in the task worktree via
proc_group (tree-kill) + an optional read-only smoke (/health, /status, /queue
+ serial_gate), maps the exit-code -> result: PASS|FAIL via the existing
self_deploy.map_exit_code_to_status contract, writes 13-test-report.md and
initiates the EXISTING check_tests_passed gate exactly as a finished LLM-tester.

Invariant (NFR-1): only the *producer* changes — the artifact contract
(13-test-report.md / result:), the gate check_tests_passed / _parse_tests_verdict,
STAGE_TRANSITIONS and the DB schema are byte-for-byte UNCHANGED. Additive, under
a kill-switch (test_runner_enabled), never-raise, fail-closed, self-hosting scope,
two-level outcome (tool-error DEFER, anti ORCH-110), hybrid (LLM strictly
off-control-path). 52c-`status:` is aligned with the verdict (D6.1) so the
three-field _parse_tests_verdict never false-negatives a PASS.

Docs (ORCH-118 NFR-6, atomic with code): llm-call-sites.md (A5 implemented),
llm-determinization-roadmap.md (rank 2 implemented), llm-usage-policy.md,
README/internals/overview, tester.md, CLAUDE.md, CHANGELOG.md. Coverage:
tests/test_orch116_test_runner.py (TC-01..TC-14); LLM anti-drift tests green.
Full suite: 2137 passed.

Refs: ORCH-116
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:37:40 +03:00
74f53b522a architect(ET): auto-commit from architect run_id=739 2026-06-16 09:36:50 +03:00
9e543551aa analyst(ET): auto-commit from analyst run_id=738 2026-06-16 09:36:50 +03:00
c081a5b6ff docs: init ORCH-116 business request 2026-06-16 09:36:50 +03:00
9c88fdd85e Merge pull request 'feat: ORCH-123-bug-staging-runner-assumes-doc' (#143) from feature/ORCH-123-bug-staging-runner-assumes-doc into main 2026-06-16 09:03:30 +03:00
deploy-finalizer
031130c7f0 deploy(ORCH-036): finalize SUCCESS for ORCH-123
All checks were successful
CI / test (push) Successful in 1m8s
2026-06-16 09:03:29 +03:00
12e3a9e4f3 docs(ORCH-123): staging gate log — staging_status SUCCESS (8/10, C9a/C9b infra-waived)
All checks were successful
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m14s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:57:48 +03:00
2b71f3887f tester(ET): auto-commit from tester run_id=752
All checks were successful
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m8s
2026-06-16 08:55:07 +03:00
820e534e77 reviewer(ET): auto-commit from reviewer run_id=751
All checks were successful
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m12s
2026-06-16 08:52:22 +03:00
cc41dd849c fix(staging): host-side ssh execution + env classification for staging-runner (ORCH-123)
All checks were successful
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m8s
The ORCH-115 deterministic staging-runner ran `docker exec` FROM INSIDE the prod
`orchestrator` container, which ships only `openssh-client git curl` — no `docker`
CLI (Dockerfile:11). `Popen(["docker", ...])` hit FileNotFoundError -> a PERMANENT
environment defect that was mis-routed as a code-fail rollback
`deploy-staging -> development` (burning developer-retries). Incident ORCH-116:
every self-hosting task reaching deploy-staging was doomed to a false rollback.

Fix (adr-0049, additive, flag-gated, never-raise, self-hosting scope; the gate /
artifact contract / STAGE_TRANSITIONS / DB schema are byte-for-byte unchanged):

- D1: build_staging_command() wraps the SAME `docker exec ... staging_check.py
  ... --mode stub` in `ssh <user@host> '<...>'` so it runs HOST-SIDE over the
  existing trusted ssh channel (mirror self_deploy / image_freshness). New flag
  staging_runner_exec_host_side (default True). No docker CLI/SDK added to the
  image, docker.sock not used in-container (D2 security).
- D3: three-way classify_staging_outcome (suite-ran / permanent-env /
  transient-infra), disambiguating the exit=1 collision by scanning stderr.
- D4: invariant "infra != code-fail" — permanent-env / exhausted transient-infra
  end in an infra-HOLD (no rollback, no developer-retry), NOT a false FAILED
  rollback (supersedes ORCH-115 D5). A really-executed failing suite still rolls
  back (anti-over-tolerance). R-2 verified: a held deploy-staging task is not
  rolled back by the reconciler.
- D5: prod-like preflight() of the host-side channel at startup (main.lifespan,
  best-effort, never blocks).
- D8: snapshot adds permanent_env / exec_host_side / preflight.

Docs (golden source, same PR): INFRA.md execution-boundary section,
architecture/README.md, CLAUDE.md, CHANGELOG.md, .env.example.

Tests: tests/test_orch123_staging_runner_exec.py (TC-01 mandatory regression
red->green; TC-02..TC-14 + R-2). ORCH-115 anti-drift green (3 tests updated for
the D1/D4/D8 supersession). Full suite: 2131 passed.

Refs: ORCH-123

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:42:36 +03:00
e1872e3d94 developer(ET): auto-commit from developer run_id=749
All checks were successful
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m7s
2026-06-16 08:17:25 +03:00
2a47744c9d architect(ET): auto-commit from architect run_id=748
All checks were successful
CI / test (push) Successful in 1m8s
2026-06-16 08:07:55 +03:00
3865b14a1c analyst(ET): auto-commit from analyst run_id=747
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-16 07:55:43 +03:00
65c17d85e3 Merge pull request 'feat(staging): deterministic staging-runner replacing LLM deployer on deploy-staging (ORCH-115)' (#141) from feature/ORCH-115-orch-replace-llm-deployer-with into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-16 02:21:03 +03:00
deploy-finalizer
17312ac86f deploy(ORCH-036): finalize SUCCESS for ORCH-115
All checks were successful
CI / test (push) Successful in 1m8s
2026-06-16 02:21:02 +03:00
a975591a3c deploy-staging(ORCH-115): staging gate SUCCESS (8/10 PASS, C9a/C9b infra-waived)
All checks were successful
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m14s
2026-06-16 02:15:21 +03:00
aed3ba0cbb tester(ET): auto-commit from tester run_id=736
All checks were successful
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m7s
2026-06-16 02:11:37 +03:00
e3ce01b824 reviewer(ET): auto-commit from reviewer run_id=735
All checks were successful
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m13s
2026-06-16 02:08:18 +03:00
b50cf1dd08 feat(staging): deterministic staging-runner replacing LLM deployer on deploy-staging (ORCH-115)
All checks were successful
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m8s
Replace the LLM `deployer` agent on the `deploy-staging` stage (self-hosting
orchestrator) with a deterministic staging-runner intercepted in launch_job
BEFORE _spawn (the deploy-finalizer / post-deploy-monitor reserved-agent
precedent). The runner executes the SAME staging suite, maps the exit-code to
`staging_status:` via the existing self_deploy.map_exit_code_to_status contract,
writes 15-staging-log.md, and initiates the UNCHANGED check_staging_status gate
exactly as a finished LLM-deployer would.

Invariant (NFR-1): this replaces only the *producer* of the artifact — the
artifact contract, the gate / _parse_staging_status / check_staging_status name,
STAGE_TRANSITIONS, the machine-verdict key `staging_status:` and the DB schema are
byte-for-byte unchanged. Additive, under a kill-switch + repo-scope CSV,
never-raise, fail-safe back to the LLM path.

Two-level outcome (D5, anti ORCH-110): suite executed -> verdict -> advance
(FAILED -> the existing deploy-staging -> development rollback + developer-retry,
same as a FAILED LLM verdict); tool-error (suite did not execute) -> bounded DEFER
-> fail-closed FAILED + alert on exhaustion (infra != code fault; never a silent
advance / false green).

First implemented slice of the LLM determinization roadmap (ORCH-118 A6,
replace-deterministic-now).

- New leaf src/staging_runner.py (never-raise; proc_group tree-kill + timeout)
- launch_job intercept + _run_staging_runner_job (mirror _run_deploy_finalizer_job)
- config: ORCH_STAGING_RUNNER_* keys (enabled/repos/timeout/infra-retry budget)
- GET /queue staging_runner observability block
- docs: llm-call-sites/roadmap/usage-policy (A6 implemented; machine blocks +
  single-transport invariant intact), deployer.md (LLM branch -> fallback),
  CLAUDE.md, CHANGELOG.md, overview (tech-pipeline/tech-agents/tech-quality-security),
  .env.example
- tests/test_orch115_staging_runner.py (TC-01..TC-13); LLM anti-drift green (TC-14)

Refs: ORCH-115

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:59:43 +03:00
f120e4bd8f architect(ET): auto-commit from architect run_id=733
All checks were successful
CI / test (push) Successful in 1m9s
2026-06-16 01:37:27 +03:00
ac203c0ccf analyst(ET): auto-commit from analyst run_id=732
All checks were successful
CI / test (push) Successful in 1m6s
2026-06-16 01:11:35 +03:00
a353a72f20 docs: init ORCH-115 business request
All checks were successful
CI / test (push) Successful in 1m8s
2026-06-16 01:02:37 +03:00
e0d0c32888 Merge pull request 'ORCH-118: LLM call-site map, control-path axis, roadmap & usage policy (docs+tests only)' (#140) from feature/ORCH-118-orch-replace-avoidable-llm-con into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-16 00:42:50 +03:00
deploy-finalizer
6f4996ad31 deploy(ORCH-036): finalize SUCCESS for ORCH-118
All checks were successful
CI / test (push) Successful in 1m7s
2026-06-16 00:42:48 +03:00
b93b4587ad docs(ORCH-118): staging gate log — staging_status SUCCESS (infra-waived C9a/C9b)
All checks were successful
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m14s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:28:51 +03:00
20fbb2e202 tester(ET): auto-commit from tester run_id=730
All checks were successful
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m7s
2026-06-16 00:25:39 +03:00
5b36ca0a82 reviewer(ET): auto-commit from reviewer run_id=729
All checks were successful
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m11s
2026-06-16 00:22:52 +03:00
9710d5f80d docs(llm): LLM call-site map, control-path axis, roadmap & usage policy + anti-drift tests
All checks were successful
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m10s
ORCH-118 (inventory-first, docs+tests only): publish an evidence-based map of
every place the orchestrator's control flow consumes (or can consume) an LLM
judgment, mark the control-path axis (C control-path vs P artifact-producer),
define "avoidable LLM control path" as a checkable two-bit predicate, classify
each call-site, and order the deterministic-replacement roadmap. Pin the map to
code with offline structural anti-drift tests.

- docs/architecture/llm-call-sites.md   — map + machine-readable inventory block
  + control-path axis + classification + keep-LLM justifications + deterministic
  non-agent paths (FR-1/FR-2/FR-3/FR-8).
- docs/architecture/llm-determinization-roadmap.md — ordered candidates BY ROLE,
  savings sourced from agent_runs, recommended first slice = deployer staging
  (FR-4). No fabricated follow-up Plane-IDs (R3/NFR-6).
- docs/architecture/llm-usage-policy.md — normative principle, keep/replace
  criteria via the axis, definition of "avoidable LLM control path" (FR-5/FR-8).
- tests/test_llm_call_site_inventory.py — TC-01/02/03/04/05/06/09/12/13/14.
- tests/test_llm_determinization_docs.py — TC-07/08/11.
- CHANGELOG.md + docs/overview/tech-quality-security.md — golden-source sync (AC-8).

Avoidable LLM control paths = {tester, deployer}; control-path-keep = {reviewer};
not-control-path (P) = {analyst, architect, developer}. Single LLM transport =
launcher._spawn (S0); no alternative transport (TC-12). Runtime untouched:
STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys / DB schema are
byte-for-byte; no replacement runners implemented (FR-7). Full suite: 2081 passed.

Refs: ORCH-118
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:13:07 +03:00
7597804f8c architect(ET): auto-commit from architect run_id=727
All checks were successful
CI / test (push) Successful in 1m7s
2026-06-15 23:56:27 +03:00
70171eb1c1 analyst(ET): auto-commit from analyst run_id=726
All checks were successful
CI / test (push) Successful in 1m7s
2026-06-15 23:42:44 +03:00
55c13abb9a analyst(ET): auto-commit from analyst run_id=725
All checks were successful
CI / test (push) Successful in 1m9s
2026-06-15 23:29:04 +03:00
bc5d550965 analyst(ET): auto-commit from analyst run_id=724
All checks were successful
CI / test (push) Successful in 1m8s
2026-06-15 23:17:18 +03:00
c7bed44845 analyst(ET): auto-commit from analyst run_id=723
All checks were successful
CI / test (push) Successful in 1m10s
2026-06-15 23:01:03 +03:00
d60980c149 analyst(ET): auto-commit from analyst run_id=722
All checks were successful
CI / test (push) Successful in 1m9s
2026-06-15 22:49:43 +03:00
729caf8e2f docs: init ORCH-118 business request
All checks were successful
CI / test (push) Successful in 1m12s
2026-06-15 22:40:12 +03:00
13589fcaf4 Merge pull request 'ORCH-117: sandbox-only fail-closed guard for Plane writes from test process (fix regression ORCH-114)' (#139) from feature/ORCH-117-bug-test-staging-plane-writes- into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-15 22:32:06 +03:00
deploy-finalizer
2fc359d8fa deploy(ORCH-036): finalize SUCCESS for ORCH-117
All checks were successful
CI / test (push) Successful in 1m6s
2026-06-15 22:32:05 +03:00
f26908ffc4 tester(ET): auto-commit from tester run_id=720
All checks were successful
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m13s
2026-06-15 21:32:20 +03:00
0cb121d105 reviewer(ET): auto-commit from reviewer run_id=719 2026-06-15 21:32:20 +03:00
861b5ee984 fix(plane): sandbox-only fail-closed guard for Plane writes from test process (ORCH-117)
Close the root class of incident ORCH-114: a pytest/worktree process performed a
REAL write (PATCH issues state=<Done> + comment) against the PRODUCTION Plane
project, because test/staging processes inherit the live Plane token
(PLANE_HEADERS/PROJECT_ID are captured at import — a post-hoc env/token swap is a
no-op) and nothing forced them to write only to the sandbox. Symmetric to the
existing _no_telegram autouse floor.

- New pure never-raise leaf src/plane_write_guard.py (decide/audit_block/
  audit_allow), wired into the 3 plane_sync write primitives (update_issue_state /
  add_comment / _set_issue_state_direct) via _guard_allows_write, AT CALL TIME,
  before any network step. Active ONLY in a test process (pytest in sys.modules /
  PYTEST_CURRENT_TEST); live + staging runtimes (uvicorn) are a strict no-op.
- In a test process: default-deny. A write is allowed iff opt-in
  (plane_test_write_enabled) AND target project in the sandbox allowlist
  (plane_test_sandbox_projects, default = the one SANDBOX id). Prod is blocked even
  with opt-in (allowlist sandbox-only); unresolved project -> block (fail-closed).
- Independent second layer: tests/conftest.py::_plane_sandbox_only autouse floor.
  Intentionally NO prod-block kill-switch (anti back-door, NFR-6).
- Audit: block -> loud ERROR; sandbox-allow -> INFO.
- Bypass fixtures for the 3 (+1) pre-existing tests that assert on the mocked
  write primitive's httpx call (header/URL/state logic), the guard is no Quality
  Gate: STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / DB schema
  untouched.
- Tests: tests/test_orch117_plane_write_isolation.py (TC-01 mandatory ORCH-114
  regression + TC-02..TC-14). Docs: CLAUDE.md, architecture/README.md,
  operations/INFRA.md, .env.example, CHANGELOG.md.

Refs: ORCH-117
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:32:20 +03:00
77d3a66356 architect(ET): auto-commit from architect run_id=717 2026-06-15 21:32:20 +03:00
8ccbcb3f80 analyst(ET): auto-commit from analyst run_id=716 2026-06-15 21:32:20 +03:00
310bebb540 docs: init ORCH-117 business request 2026-06-15 21:32:20 +03:00
a9dabff539 deploy-staging(ORCH-117): staging gate SUCCESS (8/10 PASS, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:31:57 +03:00
db2fbd23e0 Merge pull request 'ORCH-114: durable transition-ownership lease + expected-stage CAS (fix double-effect/rollback↔done class)' (#138) from feature/ORCH-114-bug-pipeline-stage-transitions into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-15 19:35:58 +03:00
deploy-finalizer
eb34324852 deploy(ORCH-036): finalize SUCCESS for ORCH-114
All checks were successful
CI / test (push) Successful in 1m11s
2026-06-15 19:35:56 +03:00
7490f4fac4 tester(ET): auto-commit from tester run_id=714
All checks were successful
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m28s
2026-06-15 19:28:38 +03:00
d4eca78423 reviewer(ET): auto-commit from reviewer run_id=713 2026-06-15 19:28:38 +03:00
c4a97a7a28 fix(stage-engine): address ORCH-114 review — env/docs canon + in-region rollback CAS
Resolves the REQUEST_CHANGES findings on ORCH-114 (durable transition-ownership
lease + expected-stage CAS):

P1 — documentation = golden source:
- .env.example: add ORCH_TRANSITION_LEASE_ENABLED / ORCH_TRANSITION_LEASE_REPOS
  (canon of 100% start keys, ORCH-101), next to the other gate kill-switches.
- CLAUDE.md: add the ORCH-114 passport section (mechanism, invariant, flags,
  ADR links) so a future agent editing advance_stage/reaper/webhooks finds the
  ownership invariant in the first mandatory-read doc (ORCH-078 traceability index).

P2 — should-fix:
- docs/overview/ (system showcase, ORCH-011): add transition_lease to
  tech-data-model.md (helper tables), tech-observability.md (/queue blocks) and
  tech-architecture.md (components).
- ADR-001 D4 alignment: the four side-effectful-edge rollback handlers
  (_handle_merge_gate_rollback / _handle_security_gate / _handle_coverage_gate /
  _handle_image_freshness) now write `development` through the expected-stage CAS
  via a shared _rollback_stage_cas helper (defence against the rollback↔done
  contradiction, BR-6) instead of a bare unconditional update_task_stage. Under the
  held lease the sole owner always wins; a lost race aborts WITHOUT side effects.
  Kill-switch off / out-of-scope repo -> degenerates to the prior write -> 1:1.
- Test isolation: make tests/test_webhooks.py order-independent by pinning the
  proj-1 registry per-test (mirrors test_webhook_dedup.proj_registry); it had only
  passed by relying on import order. Drop the needless module-level ORCH_DB_PATH
  setdefault in test_orch114 (fresh_db already isolates db_path).

New regression tests (TC-11): in-region rollback writes route through CAS;
rollback CAS wins when at expected stage; rollback CAS-lost does NOT clobber `done`;
kill-switch-off rollback degenerates to the unconditional write.

ruff clean (src/stage_engine.py, src/transition_lease.py); full suite 2052 passed.

Refs: ORCH-114
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:28:38 +03:00
4a6b32e61d reviewer(ET): auto-commit from reviewer run_id=711 2026-06-15 19:28:38 +03:00
6ea4402942 fix(stage-engine): durable transition-ownership lease + expected-stage CAS (ORCH-114)
Close the root class of the ORCH-110/111/112/113 incident chain: side-effectful
stage transitions had no single ownership. `advance_stage` is re-enterable and wrote
the stage with a bare `UPDATE ... WHERE id=?` (no compare-and-swap), while >=5 actors
(monitor / Plane-webhook / reconciler F-1 / job-reaper / deploy-finalizer) enter the
same transition independently. A concurrent or post-restart re-entry therefore
re-applied irreversible effects (merge_pr / coverage-ratchet / image-rebuild /
prod-deploy initiation) and produced a contradictory rollback<->done (incident
ORCH-111, job 1914 / PR #130).

Two complementary layers, both additive, under one kill-switch, never-raise:
  1. Durable transition-lease (new table `transition_lease`) — owner-exclusion on
     ENTRY to the side-effectful region: a second actor that sees a LIVE owner does
     not start the heavy sub-gates at all (prevention, not post-hoc repair).
  2. Expected-stage CAS (`db.update_task_stage_cas`) — atomicity on the stage WRITE:
     a lost race aborts with NO side effect. Also closes the 6 paths that write the
     stage in bypass of advance_stage (gitea x5 + plane rollback).

Owner liveness = owner_pid + owner_boot_id (NOT a heartbeat — a blocking 900s merge
re-test cannot beat one; ADR-001 D3), making restart recovery free (a fresh boot_id
renders every prior lease stale -> reclaimed by recover_on_startup). The lease has no
own TTL: its hard age ceiling is the reaper Tier-3 backstop reaper_max_running_s, so
the cross-cutting budget invariant ORCH-065/109/110/113 is untouched.

Generalises ORCH-113 finalizer-liveness (process-local, Tier-2, deploy-staging) to a
durable cross-path lease: the reaper consults it on all relevant paths (defer live,
reclaim dead; Tier-3 ignores the marker -> bounded; a reap force-releases the lease);
reconciler F-1 and the Plane webhook defer on an active lease; main.lifespan calls
recover_on_startup() after requeue_running_jobs. finalizer_liveness.py is unchanged
(it remains the kill-switch-off fallback).

Scope self-hosting (transition_lease_repos="" -> orchestrator only; enduro untouched).
Kill-switch ORCH_TRANSITION_LEASE_ENABLED=false -> CAS degenerates to the prior
unconditional update_task_stage, lease inert, reaper -> ORCH-113 fallback (byte-for-
byte pre-ORCH-114). STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys /
existing table schemas — byte-for-byte (one additive table, no epoch column on tasks).

Observability: read-only `transition_lease` block in GET /queue + a Telegram alert on
forced/stale reclaim + optional POST /transition-lease/release?work_item=<id>.

Coverage: tests/test_orch114_transition_ownership.py (TC-01 mandatory regression of
the ORCH-111 class — red before fix, green after; TC-02..TC-14). Full suite green
(2048 passed); the 4 webhook tests that spied on the removed gitea.update_task_stage
were updated to spy on the new commit_stage_cas write path.

ADR: docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md
Cross-cutting: docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md

Refs: ORCH-114
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:28:38 +03:00
cc03e68847 architect(ET): auto-commit from architect run_id=709 2026-06-15 19:28:38 +03:00
9fcca9efbc analyst(ET): auto-commit from analyst run_id=708 2026-06-15 19:28:38 +03:00
ab5e4c345b docs: init ORCH-114 business request 2026-06-15 19:28:38 +03:00
6565d50242 deploy-staging(ORCH-114): staging gate SUCCESS (8/10 PASS, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:28:13 +03:00
6abb444839 Merge pull request 'ORCH-112: resilient-pull hygiene for dirty shared deploy-base (fix incident ORCH-111)' (#136) from feature/ORCH-112-bug-failed-cancelled-task-arti into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-15 15:33:19 +03:00
deploy-finalizer
285f5f05dc deploy(ORCH-036): finalize SUCCESS for ORCH-112
All checks were successful
CI / test (push) Successful in 3m9s
CI / test (pull_request) Successful in 3m11s
2026-06-15 15:33:15 +03:00
344ab72f37 tester(ET): auto-commit from tester run_id=706
All checks were successful
CI / test (push) Successful in 3m59s
CI / test (pull_request) Successful in 3m9s
2026-06-15 15:15:56 +03:00
7f673a45f7 reviewer(ET): auto-commit from reviewer run_id=705 2026-06-15 15:15:56 +03:00
a1f3b7588a fix(deploy): resilient-pull hygiene for dirty shared deploy-base (ORCH-112)
Self-deploy git pull blocked on a dirty shared main checkout (manual/abandoned
WIP from a failed/cancelled task) — incident ORCH-111: "Your local changes to
src/config.py would be overwritten by merge" wedged the prod deploy and required
manual intervention (a group risk on self-hosting).

The deploy hook (--deploy) now converges the deploy-base to a clean, current
origin/main BEFORE the pull (git fetch + reset --hard origin/main + a SCOPED
`git clean -fd`, NEVER -x), strictly preserving the rollback/log artefacts
(.deploy-prev-image-* / deploy-hook.log via -e), gitignored .env/data/*.db/build
(no -x), and sibling/.git state (out of clean scope). Gated by CHECKOUT_HYGIENE
env injected by self_deploy.build_deploy_command only when the new pure never-raise
leaf src/checkout_hygiene.py says applies(repo) (kill-switch + self-hosting scope).
Convergence after failed/cancelled is this same deploy-time self-heal — cancel_task
is NOT extended and no background janitor is introduced. Observability: the hook
writes a `hygiene` sentinel, the Phase-C finalizer reads it and sends a best-effort
Telegram alert.

Additive, under kill-switch (ORCH_CHECKOUT_HYGIENE_ENABLED, default true; off ->
bare `git pull origin main` 1:1 before ORCH-112), never-raise, self-hosting scope.
STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys / DB schema / the
hook exit-code contract (0/1/2, ORCH-036) are byte-for-byte untouched.

Coverage: tests/test_deploy_checkout_hygiene.py (TC-01..TC-10; real-hook shell
simulation in a temp git repo, no network/prod/ssh, + unit). TC-01 is the
mandatory ORCH-111 regression (RED before the fix, GREEN after). Docs golden
source updated in the same PR (CLAUDE.md, CHANGELOG.md, .env.example; INFRA.md /
architecture/README.md / adr-0044 written at the architecture stage).

Refs: ORCH-112

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:15:56 +03:00
31b4f3fd1d architect(ET): auto-commit from architect run_id=703 2026-06-15 15:15:56 +03:00
96b653d11c architect(ET): auto-commit from architect run_id=702 2026-06-15 15:15:56 +03:00
860de5b0a5 analyst(ET): auto-commit from analyst run_id=701 2026-06-15 15:15:56 +03:00
c086921aa1 docs: init ORCH-112 business request 2026-06-15 15:15:56 +03:00
0af5d7563c Merge pull request 'docs(ORCH-112): staging gate log artifact — SUCCESS' (#137) from deployer/ORCH-112-staging-log into main 2026-06-15 15:14:51 +03:00
eb1b7aa056 docs(ORCH-112): staging gate log artifact — SUCCESS
All checks were successful
CI / test (pull_request) Successful in 3m52s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:32 +03:00
a1544f4677 Merge pull request 'ORCH-113: reaper must not re-run deploy-staging finalization while the finalizer is alive' (#134) from feature/ORCH-113-bug-job-reaper-must-not-re-run into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-15 13:51:54 +03:00
deploy-finalizer
c8faa1ec23 deploy(ORCH-036): finalize SUCCESS for ORCH-113
All checks were successful
CI / test (push) Successful in 3m9s
CI / test (pull_request) Successful in 3m5s
2026-06-15 13:51:44 +03:00
b62e196710 developer(ET): auto-commit from developer run_id=699
All checks were successful
CI / test (push) Successful in 3m22s
CI / test (pull_request) Successful in 3m43s
2026-06-15 13:43:22 +03:00
7523b843a5 tester(ET): auto-commit from tester run_id=696
All checks were successful
CI / test (push) Successful in 4m41s
CI / test (pull_request) Successful in 4m1s
2026-06-15 13:08:41 +03:00
adeffbb39a reviewer(ET): auto-commit from reviewer run_id=695 2026-06-15 13:08:41 +03:00
7cb1f83f6c fix(reaper): do not re-run deploy-staging finalization while finalizer is alive
On the deploy-staging -> deploy edge the live monitor stamps
agent_runs.finished_at FIRST, then runs the heavy edge sub-gates
(security/merge-gate re-test/coverage/image-freshness) in-thread for MINUTES
and only THEN _finalize_job. Reaper Tier-2 measures finished_age_s from
finished_at, so past reaper_finalize_grace_s it treated the live, long
finalizer as dead and independently re-ran the advance -> a second re-test
went red -> false rollback deploy-staging -> development while the original
finalizer concurrently merged the PR (incident ORCH-111, job 1914).

Add a process-local finalizer-ownership registry (src/finalizer_liveness.py,
never-raise): the monitor mark()s ownership right after the exit_code stamp and
clear()s it in a try/finally around the (verbatim-extracted) finalization tail,
so an exception in the monitor thread still releases ownership and a genuinely
dead finalizer is reaped. The reaper Tier-2 consults the marker only when the
kill-switch is on AND the task stage == deploy-staging AND ownership is active
-> DEFER (no second advance) and fall through to the Tier-3 backstop, which
ignores the marker (a stuck/dead finalizer is still reaped in bounded time).
In-memory is authoritative (monitor + reaper are daemon threads of one uvicorn
process); restart is covered by the startup requeue_running_jobs.

Additive, global kill-switch reaper_finalizer_liveness_enabled (default True;
false -> reaper byte-for-byte prior). STAGE_TRANSITIONS / QG_CHECKS / every
check_* / machine-verdict keys / DB schema unchanged; grace/ceiling and the
ORCH-065/109/110 budget invariant untouched; never restarts prod, never pushes
main. Observability: finalizer_defers_total + finalizer_owned in GET /queue.
Tests: tests/test_orch113_reaper_finalizer_liveness.py (TC-01..TC-08, incl. the
mandatory ORCH-111 regression: red before the fix, green after).

Refs: ORCH-113

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 13:08:41 +03:00
1e74b9d042 architect(ET): auto-commit from architect run_id=693 2026-06-15 13:08:41 +03:00
425ecb7585 analyst(ET): auto-commit from analyst run_id=692 2026-06-15 13:08:41 +03:00
55e9483fb8 docs: init ORCH-113 business request 2026-06-15 13:08:41 +03:00
ae75b1650b Merge pull request 'docs(ORCH-113): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)' (#135) from docs/ORCH-113-staging-log into main 2026-06-15 13:07:50 +03:00
164cf2143c docs(ORCH-113): staging gate SUCCESS — 15-staging-log.md
All checks were successful
CI / test (pull_request) Successful in 3m56s
Staging suite 8/10 PASS, REAL failed: none; C9a/C9b infra-waived (ORCH-061).
staging_status: SUCCESS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 13:07:34 +03:00
28cd204d58 Merge pull request 'ORCH-110: merge-gate re-test infra-timeout tolerance + tree-kill + re-test contract' (#132) from feature/ORCH-110-bug-merge-gate-local-re-test-t into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-15 11:04:58 +03:00
265 changed files with 32625 additions and 225 deletions

View File

@@ -24,6 +24,19 @@ ORCH_PLANE_BOT_REVIEWER=
ORCH_PLANE_BOT_TESTER=
ORCH_PLANE_BOT_DEPLOYER=
ORCH_PLANE_BOT_STREAM=
# ORCH-117: sandbox-only fail-closed guard for Plane WRITES from a test/worktree
# process (regression of ORCH-114, where pytest mutated a live prod board issue).
# In the live runtime (uvicorn, no pytest) the guard is a no-op; in a test process
# it BLOCKS every Plane write unless BOTH the opt-in is true AND the target project
# is in the sandbox allowlist. Defaults are SAFE (default-deny): leave both as-is.
# ORCH_PLANE_TEST_WRITE_ENABLED -> opt-in for REAL Plane writes from a test process.
# false (default) = no test may write to Plane. NOT a kill-switch for the prod
# block: even true, only the sandbox allowlist below is writable (a prod write
# from pytest stays impossible).
# ORCH_PLANE_TEST_SANDBOX_PROJECTS -> CSV allowlist of sandbox project ids the
# opt-in may write to. Default = the single SANDBOX project; empty = none.
ORCH_PLANE_TEST_WRITE_ENABLED=false
ORCH_PLANE_TEST_SANDBOX_PROJECTS=8c5a3025-4f9d-4190-b79f-fa06276bb27e
# Telegram live-tracker / alerts (empty -> notifications are logged, not sent).
ORCH_TELEGRAM_BOT_TOKEN=
ORCH_TELEGRAM_CHAT_ID=
@@ -217,9 +230,28 @@ ORCH_TASK_DEPS_SOURCE=db
# 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).
# SERIAL_GATE_PAUSE_ENABLED (ORCH-124) -> per-task "park" axis. true (default) -> a
# task with tasks.paused_at NOT NULL (POST /serial-gate/pause?work_item=<id>) is
# excluded from the "active task" predicate so an URGENT successor may overtake a
# paused predecessor. TRUE no-op until an operator pauses a task. false -> pause-term
# omitted, serial-gate byte-for-byte ORCH-088/090. Scope reuses SERIAL_GATE_REPOS.
ORCH_SERIAL_GATE_ENABLED=true
ORCH_SERIAL_GATE_REPOS=
ORCH_SERIAL_GATE_FREEZE_ENABLED=true
ORCH_SERIAL_GATE_PAUSE_ENABLED=true
# ORCH-120 (adr-0053): analyst open-questions -> Needs Input. Activates the dead
# "analyst asks BLOCKING questions -> 01-questions.md -> Needs Input" path in
# _handle_analysis_approved_flow. Additive, never-raise, self-hosting scope;
# STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / DB schema UNCHANGED.
# ANALYST_QUESTIONS_GATE_ENABLED=false -> _handle_analysis_approved_flow runs its
# ORIGINAL pre-ORCH-120 order (files_ok first, then flat isfile check) byte-for-byte.
# ANALYST_QUESTIONS_GATE_REPOS (CSV) -> scope; EMPTY = self-hosting only (orchestrator).
# ANALYST_NEEDS_INPUT_AUTOPAUSE_ENABLED=true (default) -> auto-park a Needs-Input task
# (db.set_task_paused) so the repo serial-gate FIFO does not wedge while we wait for a
# human; unpark on resume. false -> operator-park only (POST /serial-gate/pause).
ORCH_ANALYST_QUESTIONS_GATE_ENABLED=true
ORCH_ANALYST_QUESTIONS_GATE_REPOS=
ORCH_ANALYST_NEEDS_INPUT_AUTOPAUSE_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)
@@ -250,6 +282,32 @@ ORCH_STOP_STATUS_REPOS=
ORCH_BUG_FAST_TRACK_ENABLED=true
ORCH_BUG_FAST_TRACK_LABEL=Bug
ORCH_BUG_FAST_TRACK_REPOS=
# ORCH-020: task-estimation side-mechanism, triggered by the operator Plane status
# «Оценка» (3rd action-status, family STOP/Confirm Deploy). A leaf src/estimator.py
# (never-raise) forecasts cost/time/tokens/story-points from the history of completed
# tasks (deterministic, NO LLM), writes the forecast to Plane (estimate_point + comment),
# the Telegram card and the additive task_estimates ledger, then returns the issue to
# Backlog. On completion the fact is written to Plane `point`. OBSERVER/PRODUCER, never a
# Quality Gate / stage. Infra precondition: create a board status «Оценка» (group
# backlog/unstarted, NEVER completed/cancelled) + a Points estimate-system 1,2,3,5,8.
# ESTIMATOR_ENABLED=false -> the «Оценка» status is not handled, nothing written
# (1:1 as before ORCH-020, zero regression).
# ESTIMATOR_REPOS (CSV) -> scope; EMPTY = self-hosting only (orchestrator).
# ESTIMATOR_MIN_SAMPLES -> history size below which the bootstrap default blends in.
# ESTIMATOR_BOOTSTRAP_* -> cold-start tokens/cost_usd/seconds when history is empty.
# ESTIMATOR_SP_COST_THRESHOLDS -> 4 ascending cost cut-offs (t1,t2,t3,t5) for the
# story-point bucket (<=t1->1 .. <=t5->5, else 8).
# ESTIMATOR_WALL_CAP_S -> cap on anomalous wall-time in history (default 24h).
# ESTIMATOR_MAX_INFLIGHT -> optional bulk-smoothing semaphore (v1 generous/off).
ORCH_ESTIMATOR_ENABLED=true
ORCH_ESTIMATOR_REPOS=
ORCH_ESTIMATOR_MIN_SAMPLES=3
ORCH_ESTIMATOR_BOOTSTRAP_TOKENS=2000000
ORCH_ESTIMATOR_BOOTSTRAP_COST_USD=3.0
ORCH_ESTIMATOR_BOOTSTRAP_SECONDS=1800
ORCH_ESTIMATOR_SP_COST_THRESHOLDS=0.50,2.00,5.00,12.00
ORCH_ESTIMATOR_WALL_CAP_S=86400
ORCH_ESTIMATOR_MAX_INFLIGHT=64
# 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
@@ -340,6 +398,15 @@ ORCH_DEPLOY_PROD_TARGET_IMAGE=orchestrator-orchestrator
ORCH_DEPLOY_PROD_COMPOSE_PROFILE=
ORCH_DEPLOY_PROD_PREV_IMAGE_FILE=.deploy-prev-image-prod
# ORCH-112: deploy-base checkout-hygiene (resilient-pull). The self-deploy hook
# converges a DIRTY shared deploy-base to a clean, current origin/main BEFORE the
# `git pull` (git fetch + reset --hard + a SCOPED `git clean -fd`, NEVER `-x`), so
# manual/abandoned WIP left by a failed/cancelled task never blocks the deploy
# (incident ORCH-111). False -> bare `git pull origin main` 1:1 as before ORCH-112.
# Empty REPOS -> only the self-hosting repo (orchestrator).
ORCH_CHECKOUT_HYGIENE_ENABLED=true
ORCH_CHECKOUT_HYGIENE_REPOS=
# 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:
@@ -425,6 +492,46 @@ ORCH_REAPER_MAX_RUNNING_S=5400
ORCH_REAPER_FINALIZE_GRACE_S=300
ORCH_LEASE_RECLAIM_ENABLED=true
# ORCH-126 (adr-0052): run-ownership hygiene of the `jobs` row — invariant
# `status='queued' => run_id IS NULL AND pid IS NULL AND started_at IS NULL`. The BASE
# reset on every requeue/claim path (requeue_running_jobs / mark_job('queued') /
# mark_job_transient / reap_running_job('queued') / claim_next_job) is UNCONDITIONAL
# (no flag — it fixes a data invariant). This kill-switch gates ONLY the optional
# detect/self-heal sweep of "impossible" queued rows (a queued job still carrying
# run_id/pid/started_at — the incident state of job 2286) run at startup + on each
# reaper tick, plus its read-only /queue counter (reaper.impossible_queued_total).
# IMPOSSIBLE_QUEUED_SANITIZE_ENABLED -> default true; false -> the sweep is a no-op
# (D1-D3 still enforce the invariant going forward).
ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED=true
# ORCH-114 (adr-0045): durable transition-ownership lease + expected-stage CAS for
# side-effectful stage transitions. Generalises the process-local ORCH-113 finalizer-
# liveness into a DURABLE, cross-path owner-exclusion (additive table `transition_lease`)
# so a concurrent OR post-restart re-entry into a side-effectful transition (reaper /
# reconciler / webhook / startup-requeue) is deferred or a no-op instead of re-applying
# an irreversible effect (merge_pr / coverage-ratchet / image-rebuild / prod-deploy
# initiation / contradictory rollback<->done). Two layers, both gated by the SINGLE
# kill-switch below: (1) a durable lease on ENTRY to the side-effectful region (a second
# actor that sees a live owner does not start the heavy sub-gates at all); (2) an
# expected-stage CAS on the stage WRITE (a lost race -> abort with NO side effect), which
# also closes the paths that write the stage in bypass of advance_stage. Owner liveness =
# owner_pid + owner_boot_id (NOT a heartbeat), so restart recovery is free (new process ->
# new boot_id -> all prior leases instantly stale -> reclaimed). The lease has NO own TTL:
# its hard age ceiling IS the reaper Tier-3 backstop (ORCH_REAPER_MAX_RUNNING_S), so the
# cross-cutting budget invariant ORCH-065/109/110/113 is untouched. STAGE_TRANSITIONS /
# QG_CHECKS / check_* / machine-verdict keys / existing table schemas — byte-for-byte.
# TRANSITION_LEASE_ENABLED -> SINGLE kill-switch. false -> the lease is neither written
# nor read AND the CAS degenerates to the prior unconditional
# update_task_stage -> behaviour byte-for-byte as before
# ORCH-114 (reaper -> ORCH-113 in-memory fallback,
# reconciler/webhook skip-guard inert). Default true.
# TRANSITION_LEASE_REPOS -> CSV scope. Empty -> applies ONLY to the self-hosting repo
# (orchestrator), where the irreversible side-effectful edges
# live; non-empty -> only the listed repos. Mirrors
# ORCH_COVERAGE_GATE_REPOS -> enduro untouched at the default.
ORCH_TRANSITION_LEASE_ENABLED=true
ORCH_TRANSITION_LEASE_REPOS=
# 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
@@ -507,6 +614,62 @@ ORCH_COVERAGE_EPSILON=0.5
ORCH_COVERAGE_TOOL_FAIL_CLOSED=false
ORCH_COVERAGE_RUN_TIMEOUT_S=900
# ORCH-115: deterministic staging-runner replacing the LLM `deployer` on the
# `deploy-staging` stage (self-hosting orchestrator). Intercepted in launch_job
# BEFORE _spawn (deploy-finalizer / post-deploy-monitor precedent): runs the same
# staging suite, maps exit-code -> staging_status:, writes 15-staging-log.md and
# initiates the UNCHANGED check_staging_status gate. Replaces only the producer of
# the artifact; the gate / STAGE_TRANSITIONS / DB schema are byte-for-byte unchanged.
# See ADR-001-deterministic-staging-runner.md / adr-0048.
# STAGING_RUNNER_ENABLED -> kill-switch; false -> the prior LLM deployer
# runs on deploy-staging via _spawn 1:1.
# STAGING_RUNNER_REPOS -> CSV scope; empty -> self-hosting only.
# STAGING_RUNNER_TIMEOUT_S -> wall-clock budget for the docker-exec suite
# (malformed/non-positive -> default 600 + WARNING).
# STAGING_RUNNER_INFRA_MAX_RETRIES -> transient-infra (timeout/ssh) bounded DEFER
# budget before an infra-HOLD (anti ORCH-110).
# STAGING_RUNNER_INFRA_RETRY_DELAY_S-> delay before the re-queued deployer job.
# STAGING_RUNNER_EXEC_HOST_SIDE -> ORCH-123 (adr-0049): true (default = prod) wraps
# the `docker exec` in `ssh <user@host> '<...>'` so
# the suite runs HOST-SIDE (the prod container ships
# no docker CLI; incident ORCH-116). false -> the
# prior in-container `docker exec` (valid only where
# a docker CLI is baked into the image). Rollback knob.
ORCH_STAGING_RUNNER_ENABLED=true
ORCH_STAGING_RUNNER_REPOS=
ORCH_STAGING_RUNNER_TIMEOUT_S=600
ORCH_STAGING_RUNNER_INFRA_MAX_RETRIES=2
ORCH_STAGING_RUNNER_INFRA_RETRY_DELAY_S=30
ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=true
# ORCH-116: deterministic test-runner replacing the LLM `tester` agent on the
# `testing` stage for the self-hosting orchestrator (2nd determinization slice,
# mirror of the ORCH-115 staging-runner). A leaf src/test_runner.py is intercepted
# in launch_job BEFORE _spawn: it runs the SAME regression `python -m pytest <target>`
# in the task worktree (+ optional read-only smoke), maps the exit-code -> result:
# PASS|FAIL, writes 13-test-report.md and initiates the UNCHANGED check_tests_passed
# gate. Replaces only the producer of the artifact; the gate / STAGE_TRANSITIONS / DB
# schema are byte-for-byte unchanged. See ADR-001-deterministic-test-runner.md / adr-0050.
# TEST_RUNNER_ENABLED -> kill-switch; false -> the prior LLM tester runs on
# testing via _spawn 1:1.
# TEST_RUNNER_REPOS -> CSV scope; empty -> self-hosting only. A repo with
# no resolvable test-contract is never intercepted (BR-9).
# TEST_RUNNER_TARGET -> pytest target of the test-contract (default tests/).
# TEST_RUNNER_TIMEOUT_S -> wall-clock budget for the pytest regression
# (malformed/non-positive -> default 900 + WARNING).
# TEST_RUNNER_SMOKE_ENABLED -> optional read-only smoke (/health,/status,/queue +
# serial_gate block); false -> pytest exit-code is the sole signal.
# TEST_RUNNER_INFRA_MAX_RETRIES -> tool-error (suite did NOT execute) bounded DEFER
# budget before a fail-closed FAIL (anti ORCH-110).
# TEST_RUNNER_INFRA_RETRY_DELAY_S-> delay before the re-queued tester job.
ORCH_TEST_RUNNER_ENABLED=true
ORCH_TEST_RUNNER_REPOS=
ORCH_TEST_RUNNER_TARGET=tests/
ORCH_TEST_RUNNER_TIMEOUT_S=900
ORCH_TEST_RUNNER_SMOKE_ENABLED=true
ORCH_TEST_RUNNER_INFRA_MAX_RETRIES=2
ORCH_TEST_RUNNER_INFRA_RETRY_DELAY_S=30
# 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".

View File

@@ -40,6 +40,21 @@ bug-report (симптом / шаги воспроизведения / лока
**сложным/архитектурным/визуальным** (нужен ADR или макет) — выпусти **полный** analysis-пакет и
помечай в bug-report `escalate: full-cycle` (эскалация в полный цикл, ADR-001 D5 ORCH-019); оператор
снимает багфикс-трек эндпоинтом `POST /bug-fast-track/escalate`.
**Блокирующие вопросы → Needs Input (ORCH-120, adr-0053).** Если бизнес-запрос **блокирующе**
неоднозначен и выпустить корректные 4 deliverables нельзя без ответа заказчика — **НЕ фабрикуй**
требования ради сдачи файлов. Вместо этого через **Write tool** запиши
`docs/work-items/<plane-id>/01-questions.md` (скелет — `docs/_templates/01-questions.md`) со списком
**конкретных** блокирующих вопросов (с вариантами и тем, что разблокирует анализ). Наличие активных
вопросов уводит задачу в **Needs Input** (движок `_handle_analysis_approved_flow` ставит статус +
комментирует вопросы в Plane) — **приоритетно** над «файлы готовы». Это сигнальный артефакт (гейтом
не парсится), пиши его ТОЛЬКО при реальных блокерах.
**Поведение на перезапуске (resume).** После ответа заказчика в Plane тебя перезапускают: прочитай
**свежие комментарии-ответы**, затем (а) если все блокеры сняты — выпусти **полный** валидный пакет
(4 файла); свежий пакет автоматически **supersedeит** старый `01-questions.md` по mtime (повторного
Needs Input не будет); (б) если часть вопросов осталась — **перепиши** `01-questions.md`, оставив
только актуальные блокеры (снова Needs Input). Не оставляй устаревшие вопросы вперемешку с новыми.
</task>
<deliverables>
@@ -52,6 +67,10 @@ bug-report (симптом / шаги воспроизведения / лока
| `03-acceptance-criteria.md` | Критерии приёмки (чёткие условия PASS/FAIL) |
| `04-test-plan.yaml` | План тестов (unit, integration; pytest) |
**When-applicable (сигнальный, ORCH-120):** `01-questions.md` — пишется **только** при блокирующих
открытых вопросах (см. `<task>`) **вместо** сфабрикованных 4 файлов; скелет —
`docs/_templates/01-questions.md`. Не machine-verdict, гейтом не парсится.
**Скелеты:** бери из `docs/_templates/` (одноимённые файлы) — не угадывай структуру.
**Эталон качества/полноты:** заполненные work item **ORCH-088** и **ORCH-073**
ориентируйся на их детальность и формат.

View File

@@ -45,6 +45,16 @@ then emit `staging_status:` / `deploy_status:`.
Run the staging test suite against the live staging environment and write the verdict.
> **ORCH-115 — deterministic runner leads this stage for in-scope repos.** On `deploy-staging` for
> the self-hosting `orchestrator` repo, this stage is now driven by **deterministic code**
> (`src/staging_runner.py`, intercepted in `launch_job` BEFORE `_spawn`, mirroring the prod Phase
> A/B/C pattern) — it runs the SAME canonical staging suite below, maps the exit code to
> `staging_status:` via the same `0 → SUCCESS / non-zero → FAILED` contract, writes
> `15-staging-log.md`, and initiates the unchanged `check_staging_status` gate. The LLM steps below
> remain the **fallback** under a disabled kill-switch (`ORCH_STAGING_RUNNER_ENABLED=false`) or for
> non-self repos. The artifact contract / gate / machine key `staging_status:` are unchanged. Details:
> `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`.
**Steps:**
1. Run the staging suite. **CANONICAL: run INSIDE the `orchestrator-staging` container via

View File

@@ -29,6 +29,17 @@ tools:
ТОЛЬКО потом выноси вердикт. Любой FAIL/смок-сбой → `result: FAIL`; всё зелёное → `result: PASS`.
</thinking>
> **ORCH-116 — детерминированный раннер ведёт эту стадию для in-scope репо.** На `testing` для
> self-hosting `orchestrator` (репо с тест-контрактом) стадию теперь ведёт **детерминированный код**
> (`src/test_runner.py`, перехват в `launch_job` **до** `_spawn`, как `deploy-finalizer`/
> `staging-runner`) — он исполняет тот же регресс `pytest tests/` в worktree ветки + read-only smoke
> (`/health`, `/status`, `/queue` + блок `serial_gate`), маппит exit-код в `result:` тем же
> контрактом `0 → PASS / иначе → FAIL`, пишет `13-test-report.md` и инициирует неизменный гейт
> `check_tests_passed`. LLM-шаги ниже остаются **fallback'ом** под выключенным kill-switch
> (`ORCH_TEST_RUNNER_ENABLED=false`) или для репо без тест-контракта. Контракт артефакта / гейт /
> machine-key `result:` — неизменны. Детали:
> `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`.
**Алгоритм:**
1. **Окружение:** `curl -s http://localhost:8500/health`.
2. **Тесты — в worktree ветки задачи, НЕ в общем `/repos/orchestrator`.** Прогоняй `pytest` из

View File

@@ -1,4 +1,4 @@
Work item: ORCH-110
Work item: ORCH-108
Repo: orchestrator
Branch: feature/ORCH-110-bug-merge-gate-local-re-test-t
Branch: feature/ORCH-108-19c40858
Stage: development

View File

@@ -3,6 +3,119 @@
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
## [Unreleased]
- **Оценка задачи, запускаемая Plane-статусом «Оценка»** (ORCH-020, `feat`): новый операторский
Plane-статус **«Оценка»** — третий член семейства action-статусов (STOP/Confirm Deploy) — запускает
**новый leaf `src/estimator.py`** (never-raise, kill-switch + скоуп), прогнозирующий
**стоимость / время / токены / сложность (story points `{1,2,3,5,8}`)** по истории завершённых задач
(детерминированная эвристика, **без LLM** — ADR-001 D1). Перевод issue в «Оценка» (в т.ч. **массово**
через Plane multi-select → N независимых вебхуков) → `handle_estimate`: прогноз (a) пишется в Plane-поле
`estimate_point` (через estimate-систему Fibonacci) + Plane-комментом, (b) добавляется пунктом «Оценка»
ремя·токены·стоимость·SP) в Telegram-карточку, (c) сохраняется в **новой аддитивной таблице**
`task_estimates` (UPSERT по `work_item_id` → идемпотентная пере-оценка), после чего issue **возвращается
в Backlog** (анти-loop: Backlog не совпадает ни с одной триггер-веткой). По завершении задачи (переход в
`done`) **факт** (из `usage.py`) пишется в Plane-поле `point` (не затирая прогноз). Анти-disruption:
issue с активным job не выдёргивается (`should_estimate`). story-points — чистая функция-бакетизатор по
стоимости (пороги конфигурируемы). **Инвариант (NFR-1/NFR-3):** оценка — наблюдатель/продюсер, **не**
Quality Gate и **не** ребро стадий — `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-ключи/схемы
существующих таблиц байт-в-байт не тронуты; горячий путь `resolve_agent_model`/`_spawn` не трогается.
Fail-closed: ключ `estimate` отсутствует в `_DEFAULT_STATES` → доска без статуса → ветка инертна
(зеркало STOP/Confirm Deploy). Опц. эндпоинты `POST /estimate`/`GET /estimate` (то же ядро) + read-only
блок `estimator` в `GET /queue`. Флаги `ORCH_ESTIMATOR_*` (`enabled` kill-switch, `repos` CSV — **пусто →
self-hosting only**, bootstrap-дефолты, пороги story-points). Откат = `ORCH_ESTIMATOR_ENABLED=false`
модуль инертен (нулевая регрессия; enduro не затронут). Онбординг-канон расширен 23-м статусом «Оценка»
(группа `unstarted`, НЕ терминальная). Витрина системы `docs/overview/` обновлена под новую
операторскую способность «Оценка» (бизнес-способность + сценарий в `business.md`, слайды
`presentation.md`, статус-жест в `tech-pipeline.md`/`tech-integrations.md`, таблица
`task_estimates` в `tech-data-model.md`, блок `estimator`/пункт карточки в `tech-observability.md`;
переформулировано устаревшее «управляющих статусов ровно три» → семейство операторских
статусов-действий). Покрытие — `tests/test_orch020_estimator.py` (TC-01…TC-20) +
`tests/test_orch020_estimator_branches.py` (ветви never-raise / fail-safe / edge: история-форкаст,
bootstrap-блендинг, все `except`-арки leaf'а, малформ-входы бакетизатора, опц. `/estimate`-эндпоинты,
парс estimate-системы Plane и degenerate-пути вебхука — `src/estimator.py` 74%→99.5%, гейт покрытия
`src/` зелёный). Детали —
`docs/work-items/ORCH-020/06-adr/ADR-001-task-estimation-status-trigger.md`, сквозной
`docs/architecture/adr/adr-0054-task-estimation-status-trigger.md`.
- **FAQ по статусу STOP для пользователя доски** (ORCH-108, `docs`): создан пользовательский
FAQ `docs/operations/FAQ_STOP.md` в формате «вопрос → ответ» — что делает STOP, как отменить
задачу, что происходит пошагово (агент останавливается → job'ы снимаются → рабочая ветка и
worktree удаляются → задача → `cancelled` → Telegram+Plane; **docs-артефакты сохраняются**,
`main`/прод не трогаются), отложенная отмена в критичном окне (merge/deploy), явное «STOP **не
откатывает** влитый в `main`/прод код» (revert — отдельная задача), перезапуск только через
«To Analyse» с нуля, причины no-op («ничего не произошло»), где увидеть результат, и разведение
STOP/Approved/Confirm Deploy. **docs-only:** `src/**` / `STAGE_TRANSITIONS` / `QG_CHECKS` /
`check_*` / machine-verdict / схема БД — байт-в-байт не тронуты; поведение STOP — источник истины
ORCH-090 (`adr-0026`), FAQ его лишь документирует и ссылается, не форкая (link-first: машинные
детали маркеры/lease/тумбстон — только ссылками). Добавлены двусторонние перекрёстные ссылки:
витрина `docs/overview/business.md` (Сценарий 6) и обзор `docs/overview/tech-pipeline.md`
(«Отмена: STOP → `cancelled`») → FAQ; FAQ → обзор + ADR ORCH-090. Структуру FAQ закрывает
детерминированный анти-дрейф тест `tests/test_faq_stop_doc.py` (offline, без сети/LLM/subprocess;
образец `tests/test_lite_setup_doc.py`): существование + 8 секций-якорей + факты-«кирпичи» +
кросс-ссылки + **негативный скан запрещённых утверждений на уровне предложений, а не голых
подстрок** (не фолзит на корректно отрицающих фразах — TR-3, проверено non-evergreen-самочеком).
**Норматив сопровождения:** правишь поведение STOP (`src/cancel.py` / `cancel_task` / маршрут
`stop`) → обнови `docs/operations/FAQ_STOP.md` в том же PR. ADR:
`docs/work-items/ORCH-108/06-adr/ADR-001-faq-stop-placement-and-anti-drift.md`.
- **Source-backed `00-business-request.md` вместо хардкода `TBD`** (ORCH-119, `fix`, Bug-трек): раздел «Description» файла `00-business-request.md` теперь несёт **фактический текст запроса** из Plane-issue (`description`/`description_stripped`) вместо литерала `TBD` — терялся source-backed контекст запроса. Фикс работает на **обоих** путях создания: прямой (путь A, `serial_gate` не применим — `start_pipeline` передаёт `description` в `_create_initial_docs`) и **отложенный срез ветки** (путь B, ORCH-088, доминирует на self-hosting `orchestrator`). Для пути B `description` **персистится durable** при создании задачи (аддитивная колонка `tasks.description` через `_ensure_column`, зеркало `tasks.title`, записывается **внутри того же атомарного INSERT** `create_task_atomic` — race-safe относительно анти-dup-claim ORCH-053) и читается из строки `tasks` в `launcher._spawn``_materialize_deferred_branch` на момент claim (без сетевого вызова в горячем пути, NFR-4). **Fail-safe (FR-4):** пустое/whitespace/`None`/нечитаемое описание → явный безопасный маркер `_(описание отсутствует в источнике)_` через чистый рендер-хелпер `_render_business_request` (never-raise; создание задачи не падает). **Идемпотентность:** Gitea 422 (файл существует) → no-op, ранее записанное тело не перезаписывается. **Инвариант (AC-5):** `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-ключи — байт-в-байт; единственное изменение схемы — аддитивная `tasks.description` (базовый `CREATE TABLE tasks` не тронут); анти-stale-base инвариант ORCH-088 цел (момент/условие среза не меняются — только источник данных дополняется). Обратимость — revert PR (колонка остаётся инертной). Покрытие — `tests/test_orch119_business_request.py` (TC-01 обязательный регресс red→green; TC-02…TC-07). Дополнительно в том же PR починена **тест-гермеичность** `tests/test_orch123_staging_runner_exec.py::test_r2_held_deploy_staging_not_rolled_back`: тест переиспользовал собственный (теперь смерженный в `main`) work-item id `ORCH-123`, и при дефолтном `repos_dir=/repos` staging-гейт через origin/main-fallback (`check_staging_status``_staging_log_from_main`) находил **реальный** `staging_status: SUCCESS`-лог ORCH-123 в `origin/main`, делая намеренно-красный гейт зелёным (флак проявлялся только в полном прогоне сьюта — singleton `repos_dir` берётся из первого импортирующего config файла, побеждая import-time `ORCH_REPOS_DIR=/tmp` этого модуля); autouse-фикстура `fresh_db` теперь пинит `repos_dir` в изолированный пустой tmp-каталог (зеркало уже пиннимого `worktrees_dir`), сохраняя проверяемость инварианта ORCH-123 R-2 (infra-held `deploy-staging` удерживается, не откатывается в `development`) независимо от порядка тестов. ADR: `docs/work-items/ORCH-119/06-adr/ADR-001-source-backed-business-request-doc.md`.
- **Открытые вопросы аналитика → Needs Input (приоритет, неблокирование serial-gate, resume)** (ORCH-120, `fix`, трек Bug→escalate full-cycle): активирован и достроен ранее **мёртвый** путь «аналитик задаёт блокирующие вопросы → `01-questions.md` → Needs Input». Четыре согласованных изменения, аддитивно, под kill-switch, скоуп self-hosting, never-raise; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты** (поток — pre-gate-ветка движка, **не** Quality Gate; `01-questions.md`**сигнальный** артефакт, **не** machine-verdict). (1) **Контракт + канон.** `.openclaw/agents/analyst.md` документирует канал «блокирующие вопросы → `01-questions.md`, НЕ фабриковать deliverables» + поведение на resume; новый скелет `docs/_templates/01-questions.md`; строка манифеста + примечание о префиксе `01-` в `docs/_standards/PIPELINE_DOCS.md`. (2) **Приоритет «вопросы активны» > «файлы готовы»** в `_handle_analysis_approved_flow` (DQ-3): чистая логика решения вынесена в leaf `src/analyst_questions.py` (`questions_gate_applies`/`autopause_applies`/`questions_active`), side-effects — в `stage_engine` (`_decide_analysis_outcome`/`_emit_analysis_needs_input`/`_emit_analysis_in_review`/`_emit_analysis_empty`); блокирующие вопросы достигают Needs Input даже при сфабрикованном полном пакете. (3) **Авто-park (DQ-1)** при Needs Input через ось «пауза» ORCH-124 (`db.set_task_paused`) → задача исключается из «активного» предиката serial-gate (ORCH-088), FIFO репо не клинит, пока ждём человека; **resume + unpark** в `handle_status_start` (analysis-ветка, `db.clear_task_paused`). (4) **Гигиена устаревания (DQ-2)** — детерминированный offline freshness-supersede по `mtime` (вопросы активны, пока пакет неполон ИЛИ `01-questions.md` не старше всех 4 deliverables) → полный свежий пакет supersedeит старый файл без зависимости от LLM (нет бесконечной петли Needs Input). Флаги (`config.py`, безопасные дефолты): `analyst_questions_gate_enabled` (kill-switch) / `analyst_questions_gate_repos` (CSV; **пусто → self-hosting only**) / `analyst_needs_input_autopause_enabled` (независимый тумблер авто-park/unpark; `False` → operator-park `POST /serial-gate/pause`). off/out-of-scope → байт-в-байт как до ORCH-120 (enduro не затронут); ORCH-066 (Needs Input только у аналитика) не расширяется. Покрытие — `tests/test_orch120_analyst_needs_input.py` (TC-01 обязательный регресс: красный до фикса, зелёный после), `tests/test_orch120_serial_gate_needs_input.py`, `tests/test_orch120_resume_unpark.py`, `tests/test_orch120_questions_artifact_canon.py` + assert в `tests/test_agent_prompts_canon.py`. Витрина системы `docs/overview/` обновлена в том же PR (ось ORCH-011): абзац пауз `tech-pipeline.md` и пункт `GET /queue` в `tech-observability.md` теперь называют **два** источника паузы (оператор + авто-park движком на Needs Input), `tech-agents.md` — when-applicable сигнальный канал `01-questions.md` у `analyst` (`tests/test_system_docs.py` зелёный). ADR: `docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`, сквозной `docs/architecture/adr/adr-0053-analyst-open-questions-needs-input-flow.md`.
- **Гигиена run-ownership строки `jobs` — инвариант «queued ⇒ run_id/pid/started_at IS NULL»** (ORCH-126, `fix`, трек Bug): багфикс контрол-плейна (инцидент ORCH-124/125) — при `ORCH_SERIAL_GATE_ENABLED=false` queued analyst-job'ы зависали навсегда (job 2286: `status=queued + run_id=759/760 + pid=35/42 + started_at=NULL` — физически невозможное состояние). **Причина:** ни один путь возврата job в `queued` (restart `requeue_running_jobs` / retry `mark_job('queued')` / transient `mark_job_transient` / reap `reap_running_job('queued')`) **не сбрасывал run-ownership** (`run_id`/`pid`); после рестарта контейнера pid мог быть **переиспользован** ОС`pid_alive(stale)=True` → job-reaper (ORCH-065) Tier-1 «видел живой» фантомный `running` и при `max_concurrency=1` клинил клейм **всей** общей очереди всех проектов. **Инвариант (adr-0052):** `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL` — queued-job никогда не несёт run-ownership (история run'а — в `agent_runs`, не в `jobs.run_id`). Фикс на **существующих колонках**: `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи / **схема БД** — байт-в-байт не тронуты; для здоровых job'ов и enduro поведение байт-в-байт; миграция не требуется. ADR: `docs/work-items/ORCH-126/06-adr/ADR-001-queued-job-run-ownership-hygiene.md`, сквозной `docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md`.
- **D1 — Forward-cleanup на всех путях возврата в `queued` (FR-1/AC-1):** `requeue_running_jobs` / `mark_job('queued')` / `mark_job_transient` / `reap_running_job('queued')` выставляют `run_id=NULL, pid=NULL` той же UPDATE-транзакцией, что чистит `started_at`/`finished_at`. Атомарные `status`-guard'ы (`reap_running_job … WHERE status='running'`, rowcount) — **сохранены байт-в-байт** (restart-safe, гонка worker↔reaper↔monitor — TR-4). Каллер-переданный `run_id` для `queued` **игнорируется** (инвариант важнее: `launcher._finalize_permanent`/reaper по-прежнему передают старый `run_id`, но для `queued` он сбрасывается). Безусловно — исправление инварианта данных, без флага (D6).
- **D2 — Чистый claim (FR-2/AC-3):** `claim_next_job` при флипе `queued→running` сбрасывает `pid=NULL, run_id=NULL` тем же существующим UPDATE (defense-in-depth поверх D1) → между claim и стампом `pid` в `_spawn` строка несёт `pid IS NULL`, не чужой pid. SELECT-гейт (`status='queued' AND available_at<=now` + dep/serial-gate) — **не тронут** (offline hot-path, NFR-2; без нового SELECT/сети).
- **D3 — Окно `_spawn` (FR-3/AC-6):** провал `_spawn` до стампа `pid` (`ensure_worktree`/материализация ветки/запись task-файла) → `queue_worker._drain_once` возвращает job через `mark_job('queued')` → по D1 строка чистая; повторный claim стартует штатно (без «частично стартовавшего» зависания). Нового кода в launcher не потребовалось.
- **D4 — Детект + self-heal невозможного состояния (FR-4/AC-5):** `db.find_impossible_queued_jobs()`/`db.sanitize_impossible_queued()` идемпотентно приводят «невозможные» queued-строки (`queued` с непустым `run_id`/`pid`/`started_at`) к чистому `queued`; вызывается при старте (`main.lifespan` после `requeue_running_jobs`) и на каждом реап-тике (`JobReaper.sanitize_impossible_queued_once`, never-raise) — закрывает уже-существующие аномалии на проблемной БД **без миграции** (TR-7) и забытый будущий 6-й путь возврата (TR-2). Наблюдаемость: структурный WARNING (`job_id`/`run_id`/`pid`) + read-only счётчик `impossible_queued_total`/`last_impossible_queued` в блоке `reaper` снимка `GET /queue`. Kill-switch `impossible_queued_sanitize_enabled` (env `ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED`, дефолт on; гейтит **только** D4-sweep, базовый сброс D1-D3 безусловен).
- **D5 — Корректность reaper-liveness (FR-5/AC-4) — валидация, не правка:** после D1-D3 reaper на свежеклеймленном `running` видит `pid IS NULL` → Tier-1 (`job_reaper.py:245`: `if pid is not None and not pid_alive(pid)`) пропускает, сбрасывает streak; Tier-3 backstop (`reaper_max_running_s`) — без изменений. **Правка reaper не требуется** — фикс восстанавливает предусловие «`pid` отражает процесс ЭТОГО run'а». Маркированные инварианты ORCH-065/113/114/099 — сохранены (трассировка CLAUDE.md §9).
- **Покрытие:** `tests/test_orch126_queued_stale_run.py` (TC-01 — обязательный регресс, КРАСНЫЙ до фикса / ЗЕЛЁНЫЙ после: stale `running``requeue_running_jobs` → чистый `queued`; TC-02…TC-10: сброс на каждом пути, чистый claim, claim без старвейшна при serial-gate off, reaper не реапит `pid IS NULL`, self-heal идемпотентность + счётчик + kill-switch, окно `_spawn`, анти-регресс здорового job'а — терминальные исходы/`run_id`-линк не затронуты). Полный `pytest tests/ -q` — зелёный.
- **Доки:** `docs/architecture/internals.md` (раздел «Инвариант run-ownership строки `jobs`» + аннотации `jobs.run_id`/`pid` + queue-recovery), `.env.example` (флаг `ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED` в блоке reaper); сквозной ADR `adr-0052` (уже заведён архитектором).
- **Serial-gate «пауза без блокировки» — явный per-task park-сигнал** (ORCH-124, `fix`): багфикс (метка `Bug`, эскалирован в full-cycle) инцидента **ORCH-116/ORCH-123**. `serial_gate` определял «активную задачу репо» **исключительно по машинной стадии** `tasks.stage NOT IN ('done','cancelled')`, а Plane-статусы Backlog/Blocked/Needs-Input (слой B индикации, ORCH-066) **не меняют `tasks.stage`** (слой A) ⇒ приостановленный предшественник был неотличим от активного и держал FIFO-гейт закрытым против срочного успешника (ORCH-116 поставлен на паузу, чтобы пропустить фикс ORCH-123 — фикс не стартовал, пока ORCH-116 формально не `done`). У оператора не было чистого механизма «пауза без блокировки», отдельного от cancel (терминал) и от глобального выключения гейта. **Инвариант:** правка **планировщика очереди** (claim) и наблюдаемости, **не** Quality Gate — `STAGE_TRANSITIONS` / состав `QG_CHECKS` / семантика и имена `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`) / схемы существующих таблиц — **байт-в-байт не тронуты**. Аддитивно, под независимым под-флагом, never-raise, restart-safe, fail-OPEN на hot-claim сохранён. ADR: `docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`, сквозной `docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md`.
- **Механизм (D1):** явный durable DB-сигнал «park» на уровне задачи, инициируемый оператором через API — **не** маппинг Plane-статуса (перегружал бы слой A/B ORCH-066 / анти-паттерн ORCH-059) и **не** `task_deps` (моделирует обратное направление «B ждёт A»). Чистое намерение, отличное от cancel и от kill-switch; DB-резолвимо, offline, webhook-независимо (потерянный webhook не рассинхронит сигнал).
- **Хранилище (D2):** аддитивная нуллабельная колонка `tasks.paused_at TEXT` через `_ensure_column` (паттерн `tasks.cancelled_at`/`cancel_requested_at`/`track`; `src/db.py`) — NULL = не на паузе; ISO-таймстамп = поставлена оператором на паузу. На уже-мигрированной БД — no-op; все существующие строки дефолтят в NULL ⇒ поведение до ORCH-124 до первой явной паузы (enduro не затронут на общей прод-БД). Хелперы `db.set_task_paused`/`clear_task_paused`/`is_task_paused` (never-raise; `is_task_paused` на ошибке → «не на паузе» = задача активна = гейт скорее закрыт = анти-stale-base-safe).
- **Ортогональная ось (D3, критично):** «активность» для serial-gate = `stage NOT IN ('done','cancelled') AND paused_at IS NULL`; **терминал `{done,cancelled}` остаётся байт-в-байт** в `serial_gate`/`task_deps`/`stages.py` (adr-0026 не регрессирует). `task_deps`/`stages.py` колонку `paused_at` **НЕ читают** ⇒ паузнутая объявленная зависимость (`job_deps`) и `repo_freeze` **по-прежнему блокируют** claim (пауза их **не** обходит — разные оси: freeze = весь репо, dependency = конкретная пара, пауза = «пропустите меня в FIFO»).
- **Три точки согласованно (D4, анти-дрейф):** один предикат «активна» под под-флагом — терм `AND t2.paused_at IS NULL` внутри существующего `EXISTS`-подзапроса `build_claim_clause` (горячий offline SQL, без лишнего JOIN), `AND paused_at IS NULL` в `repo_has_active_task` и в выборе `active_task` `_per_repo_snapshot` (`src/serial_gate.py`). Помечено маркером `ORCH-124` рядом с `ORCH-088`/`ORCH-090`.
- **Операторские эндпоинты (D7):** `POST /serial-gate/pause?work_item=<id>` (стамп `paused_at`; терминальная/неизвестная задача → no-op-ответ; под-флаг off → no-op-предупреждение) и `POST /serial-gate/resume?work_item=<id>` (сброс `paused_at` → задача снова участвует в гейте; идемпотентно) — по образцу `POST /serial-gate/unfreeze`, never-raise, с Telegram-подтверждением (`src/main.py`).
- **Анти-stale-base при resume (D8, R-1):** новой rebase-машинерии **нет** — свежесть базы гарантируют существующие механизмы: паузнутая-в-`analysis` задача при resume режет ветку отложенно (ORCH-088) от свежего `origin/main` с кодом успешника; материализованная — ребейзится на merge-gate (`auto_rebase_onto_main` под merge-lease ORCH-026/093) + re-test (ORCH-110). Нормальная задача (`paused_at IS NULL`) по-прежнему держит гейт ⇒ анти-stale-base для нормального случая (ORCH-088) **не регрессирует**.
- **Наблюдаемость (D5):** блок `serial_gate` в `GET /queue` дополнен ключом `paused` (список приостановленных незавершённых задач репо — НЕ показываются как `active_task`) и `reason` ожидания у каждого waiting-job с приоритетом `freeze``dependency``active-task``null`; существующие ключи снапшота (`active_task`/`waiting`/`frozen`/`frozen_reason`/`frozen_at`) — байт-в-байт (BC).
- **Условность/откат (D6):** независимый под-флаг `serial_gate_pause_enabled` (env `ORCH_SERIAL_GATE_PAUSE_ENABLED`, дефолт `True`; зеркало `serial_gate_freeze_enabled`; область переиспользует `serial_gate_repos`, новый `*_repos` не вводится). Дефолт `True`**истинный no-op** до явной операторской паузы (`paused_at` всюду NULL). `False` ⇒ pause-терм опущен из SQL, эндпоинты no-op, serial-gate **байт-в-байт ORCH-088/090** (осознанный rollback-режим). Глубже — `serial_gate_enabled=false`.
- **Покрытие:** `tests/test_orch124_serial_gate_pause.py` (TC-01 обязательный регресс инцидента ORCH-116/ORCH-123 — красный до фикса, зелёный после; TC-02…TC-15: анти-регресс ORCH-088, durable/restart, resume, сохранность freeze/dependency, снапшот-reason, анти-дрейф 3 точек, offline hot-path, never-raise/fail-OPEN, kill-switch-нейтральность, структурный анти-регресс реестров/схем).
- **Доки:** обновлены `docs/architecture/README.md` (раздел serial-gate + ось «пауза без блокировки») и `docs/architecture/internals.md` (ось «пауза» ⊥ оси «терминальность»); сквозной ADR `adr-0051`. **Витрина системы `docs/overview/` (ORCH-011, синхронно в том же PR):** `tech-pipeline.md` (исключение FIFO «пауза без блокировки» рядом с freeze), `tech-data-model.md` (durable-сигнал `tasks.paused_at`), `tech-observability.md` (`paused`/`reason` в блоке `serial_gate` `GET /queue` + эндпоинты `pause|resume`). Зачищены протёкшие хвостовые теги tool-call (`</content>`/`</invoke>`) в 4 golden-source доках этого PR (`06-adr/ADR-001`, `adr-0051`, `08-data-requirements.md`, `10-tech-risks.md`).
- **Тест-гигиена (development-стадия, латентный регресс ORCH-123):** изолирован `settings.repos_dir` в фикстуре `tests/test_orch123_staging_runner_exec.py` (зеркально уже имевшейся изоляции `worktrees_dir`). `check_staging_status` при отсутствии фиче-worktree фолбэчит на `<repos_dir>/<repo>`его `origin/main`); после мержа ORCH-123 реальный `/repos/orchestrator/docs/work-items/ORCH-123/15-staging-log.md` (вердикт SUCCESS) появился на диске и делал предполагавшийся-КРАСНЫМ staging-гейт в `test_r2_held_deploy_staging_not_rolled_back` зелёным при полном прогоне `pytest tests/` (order-dependent: тест проходил в одиночку, падал в сьюте). Инвариант ORCH-123 R-2 («held `deploy-staging` не откатывается на `development`», adr-0049/ADR-001 D4) **сохранён и усилен** — изоляция лишь восстанавливает заявленную предпосылку теста «15-staging-log.md отсутствует ⇒ гейт красный». `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*` не тронуты (правка только теста).
- **Детерминированный test-раннер вместо LLM-тестера на `testing`** (ORCH-116, `feat`): второй реализованный срез determinization-roadmap (ORCH-118 A5, `needs-hybrid-fallback`) — на стадии `testing` для self-hosting `orchestrator` **LLM-агент `tester` заменён детерминированным кодом** (`src/test_runner.py`). PASS/FAIL-ядро агента было деривируемым (регресс `pytest` + read-only smoke → `result:`); каждый прогон жёг токены/время opus-агента (~60150k / 520 мин) и встраивал недетерминизм LLM в точку ветвления `testing → deploy-staging` / `testing → development`. **Инвариант (NFR-1):** это замена *продюсера* артефакта, **не** гейта — контракт `13-test-report.md`, гейт `check_tests_passed`/`_parse_tests_verdict`, `STAGE_TRANSITIONS`, machine-verdict `result:` (+ legacy `verdict:`/`status:`), схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, скоуп self-hosting, гибрид (LLM строго off-control-path). Эталон — `src/staging_runner.py` (ORCH-115). ADR: `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной `docs/architecture/adr/adr-0050-deterministic-test-runner.md`.
- **Перехват в `launch_job` до `_spawn` (D1):** `if job.agent=="tester" and test_runner.should_intercept(job)``_run_test_runner_job` (зеркало `_run_staging_runner_job`, прецедент `deploy-finalizer`/`post-deploy-monitor`/`staging-runner` `launcher.py:397/402/405`): синхронно ведёт `jobs`-строку через `mark_job`, возвращает `None` (нет `agent_runs`, нет токенов). Дискриминатор — роль `tester` **И** стадия задачи `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`, коллизии стадий нет, в отличие от общей роли `deployer`) **И** `applies(repo)`; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Leaf `src/test_runner.py` (новый, чистый never-raise):** по образцу `staging_runner`/`self_deploy`/`proc_group` (на импорте только `config`/`proc_group`; `db`/`git_worktree`/`self_deploy`/`qg.checks`/`stage_engine`/`notifications` — лениво). `applies(repo)` = kill-switch `test_runner_enabled` + скоуп `test_runner_repos` (пусто → self-hosting only) **И** резолв тест-контракта `_has_test_contract` (BR-9: репо без контракта → `False` → LLM-tester — enduro-trails 1:1 как до ORCH-116, даже если руками добавлен в CSV). Исполняет регресс `python -m pytest <test_runner_target>` **в worktree ветки** (`git_worktree.get_worktree_path`, анти checkout-гонка ORCH-112) через `proc_group.run_in_process_group` (tree-kill, таймаут `test_runner_timeout_s=900`, малформ/непозитив → дефолт + WARNING) + опц. **read-only smoke** (`/health`/`/status`/`/queue` + блок `serial_gate`, stdlib `urllib`; транзиентная недостижимость — ограниченный ретрай, не-200/нет блока — немедленный FAIL; `test_runner_smoke_enabled`). Маппит exit-код **единым** контрактом `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе/None→`FAIL`, fail-closed; smoke-провал AND-ится в `FAIL`); пишет `13-test-report.md` (тот же machine-key `result:` UPPERCASE + 52c-схема, `author_agent: test-runner`/`model_used: n/a`) + best-effort push в **фичеветку**; вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")` — без новых рёбер/исходов (transition-lease ORCH-114 берётся внутри `advance_stage` — граница O1).
- **Анти-коллизия 52c-`status:` ↔ парсер (D6.1, специфично для tester):** `_parse_tests_verdict` читает вердикт из **трёх** равноранговых полей (`verdict:`/`status:`/`result:`) с negative-token-priority. 52c-обязательное `status:` поэтому читается тем же парсером → раннер **ВСЕГДА выравнивает** `status:` по вердикту (`PASS → status: success`, `FAIL → status: failed`) — иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL здорового прогона. Прибито unit-тестом через **неизменённый** парсер.
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→advance (FAIL → тот же откат `testing → development` + developer-retry, что у FAIL-вердикта LLM, `stage_engine.py:849`); сюита **не исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл → bounded **DEFER** (re-queue **`tester`**-джоба с задержкой + restart-safe маркер `test-runner infra-retry` в `task_content`, счётчик подсчётом маркера — без правки схемы БД), на исчерпании `test_runner_infra_max_retries=2` → fail-closed `FAIL` + advance + INFRA-alert. Раннер **никогда** не делает тихий advance/ложный green, **никогда** не клинит очередь, **не** жжёт developer-retry на транзиентной инфре.
- **Гибрид (D11/BR-8/NFR-7):** в Phase 1 на `testing` (in-scope) вердикт `result:` производит **только** детерминированный раннер; LLM **не** вызывается в потоке управления вердикта. Архитектура не запрещает будущий **off-control-path** LLM-триаж падений / маппинг TC↔критерии после детерминированного FAIL (отдельная роль/джоб, **не** выносит и **не** переопределяет `result:`, **не** добавляет ребро в `STAGE_TRANSITIONS`) — в этом срезе не реализуется. Self-hosting safety: в командах раннера нет рестарта 8500 / `docker compose up orchestrator` / `--build` / force-push / правок `.env`; smoke строго read-only. Наблюдаемость — in-process счётчики (`runs`/`pass`/`fail`/`tool_error`/`deferred`) + read-only блок `test_runner` в `GET /queue` + один структурный лог-вердикт на прогон (различает код-фейл и tool-error). Флаги (`config.py`, дефолт = боевое): `test_runner_enabled`/`test_runner_repos`/`test_runner_target`/`test_runner_timeout_s`/`test_runner_smoke_enabled`/`test_runner_infra_max_retries`/`test_runner_infra_retry_delay_s` (env `ORCH_TEST_RUNNER_*`). Откат = `ORCH_TEST_RUNNER_ENABLED=false` → на `testing` снова LLM-`tester` через `_spawn` **байт-в-байт**.
- **Норматив сопровождения ORCH-118 (NFR-6):** обновлены `docs/architecture/llm-call-sites.md` (A5 — реализован; машинный `ORCH-118-INVENTORY-BLOCK` сохраняет tester как `avoidable=yes`/`axis=C`/`needs-hybrid-fallback`), `llm-determinization-roadmap.md` (rank 2 tester — ✅ реализован; инвариант «ровно один `first_slice = yes`» у rank 1 deployer цел), `llm-usage-policy.md` (§5 — единственный транспорт S0 не нарушен, раннер LLM не зовёт), `.openclaw/agents/tester.md` (LLM-ветвь `testing` — fallback), `docs/architecture/README.md`/`internals.md`, витрина `docs/overview/tech-pipeline.md`/`tech-agents.md`/`tech-quality-security.md`. Покрытие — `tests/test_orch116_test_runner.py` (TC-01…TC-14) + зелёные `tests/test_llm_call_site_inventory.py`/`test_llm_determinization_docs.py` (TC-15).
- **Host-side исполнение staging-раннера + классификация environment-дефекта** (ORCH-123, `fix`, bug→escalate full-cycle): устранён инцидент **ORCH-116** — детерминированный staging-раннер (ORCH-115) вызывал `docker exec` **изнутри** прод-контейнера `orchestrator`, где **нет бинаря `docker`** (образ несёт только `openssh-client git curl`, `Dockerfile:11`; `/var/run/docker.sock` смонтирован, но клиента нет) → `Popen(["docker", …])` падал `FileNotFoundError` → ветка tool-error → инфра-DEFER×2 → fail-closed `FAILED`**ложный** откат `deploy-staging → development` (как код-фейл, с расходом developer-retry). Так до фикса **любая** self-hosting задача, дойдя до `deploy-staging`, была обречена на ложный откат. Аддитивно, под флагами, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_staging_status`/`_parse_staging_status` / machine-verdict-ключи (`staging_status:`/`deploy_status:`) / схема БД — **байт-в-байт не тронуты** (замена *стратегии исполнения продюсера* `15-staging-log.md`, **не** гейта/стадии; зеркало инварианта ORCH-115 NFR-1). ADR: `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`, сквозной `docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`.
- **Host-side ssh-стратегия (D1):** `staging_runner.build_staging_command()` теперь обёртывает ту же `docker exec orchestrator-staging python3 … staging_check.py … --mode stub` в `ssh -o StrictHostKeyChecking=no <deploy_ssh_user>@<deploy_ssh_host> '<docker exec …>'` (зеркало `self_deploy.build_deploy_command` / `image_freshness.image_revision(ssh_target=…)`); канал — существующий доверенный (`ORCH_DEPLOY_SSH_HOST=127.0.0.1`, ssh-ключ смонтирован `:ro`, `openssh-client` в образе) → **новых секретов/привилегий не вводится** (NFR-3). Меняется **инициатор/канал** запуска, **не** сама сюита (она по-прежнему бежит **внутри** `orchestrator-staging` 8501). **Security (D2):** docker CLI/SDK в контейнер **не добавляется**, `docker.sock` **не используется изнутри** — это было бы root-эквивалентным расширением поверхности атаки (доступным и LLM-агентам); host-side ssh достигает цели без расширения привилегий.
- **Трёхсторонняя классификация исхода (D3, чистая `classify_staging_outcome`, зеркало `merge_gate.classify_retest_failure`):** `suite-ran` (распознанный exit-код, кроме 255, **без** env-маркера в stderr → доверяем коду: `0→SUCCESS`/`≠0→FAILED`; анти-over-tolerance BR-3 — реальный фейл сюиты **никогда** не реклассифицируется в инфру), `permanent-env` (spawn-error `rc=None` без таймаута / нет ssh-target / `rc∈{126,127}` / env-маркер `No such container`/`Cannot connect to the Docker daemon`/`command not found` → ретрай бессмыслен), `transient-infra` (timeout / ssh transport `rc=255` / неизвестный сигнал → ретрай осмыслен). Дизамбигуация коллизии `exit=1` (`docker exec` «No such container»=1 vs суита fail=1) — **скан stderr на env-маркеры**, не голый exit-код; fail-safe при неоднозначности → `transient-infra` (DEFER), никогда тихий `suite-ran`.
- **Маршрутизация исходов (D4) — инвариант «инфра ≠ код-фейл»:** `suite-ran`**без изменений** (ORCH-115): write `15-staging-log.md` + `advance_stage` (FAILED → прежний откат `deploy-staging → development` + developer-retry, байт-в-байт). `permanent-env`**немедленный infra-HOLD**: DEFER пропускается (FR-3), `15-staging-log.md` **не** пишется (нет ложного FAILED), `advance` **не** зовётся, developer-retry **не** жжётся; структурный ERROR + операторский alert «инфра/окружение, НЕ дефект кода». `transient-infra` → существующий bounded DEFER, **но на исчерпании бюджета** — тоже **infra-HOLD + alert** (СУПЕРСЕД ORCH-115 D5: прежний fail-closed `write_staging_log("FAILED") + advance` ложно откатывал не-прояснившуюся инфру как код-фейл, нарушая BR-2). Корневой инвариант: «сюита **не** исполнилась» (environment ИЛИ инфра) **никогда** не оканчивается код-фейл-откатом `→ development` и **никогда** не жжёт developer-retry — закрывает RCA-класс ORCH-110 на staging-ребре. Задача **держится** на `deploy-staging`; reconciler `advance_if_gate_passed` на red-гейте возвращает `None` (без отката, R-2 верифицирован) → оператор re-drive после починки окружения.
- **Prod-like preflight (D5):** `staging_runner.preflight()` (bounded, never-raise, self-hosting-скоуп — `applies()` первым) пробит host-side канал на **старте сервиса** в `main.lifespan` (best-effort, после `requeue_running_jobs`/`recover_on_startup`, **никогда не блокирует старт**): ssh-зонд `command -v docker && docker inspect -f '{{.State.Running}}' orchestrator-staging` распознаёт «нет docker на хосте» / «staging-контейнер не поднят» / «ssh недоступен» / «нет ssh-target» **до** того, как реальная задача дойдёт до ложного исхода. Чисто наблюдательный — не гейтит конвейер; лог + Telegram-алерт + поле в `snapshot()`.
- **Условность / обратимость (D6):** новый флаг `staging_runner_exec_host_side: bool = True` (env `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE`, дефолт = боевое) — `True` → host-side ssh; `False` → прежний in-container `docker exec` (валиден лишь там, где docker CLI запечён в образ). Переиспользуются `staging_runner_enabled`/`_repos`/`_timeout_s`/`_infra_max_retries`/`_retry_delay_s` + `deploy_ssh_user`/`deploy_ssh_host`. Откат — `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` (in-container 1:1) или `ORCH_STAGING_RUNNER_ENABLED=false` (LLM-deployer 1:1). Наблюдаемость (D8): счётчик `permanent_env` (infra-HOLD; отличён от `failed`=код-фейл и `deferred`=транзиент) + `exec_host_side` + `preflight` в блоке `staging_runner` `GET /queue`; один структурный лог-вердикт на прогон (`outcome ∈ {code-pass,code-fail,transient-infra,permanent-env}`).
- **Документация границы исполнения (D9):** `docs/operations/INFRA.md` (новый под-раздел «Граница исполнения docker-операций — host-side») + `docs/architecture/README.md` (host-side стратегия + трёхсторонняя классификация) — зафиксировано, что **все** docker-операции self-hosting (прод-деплой ORCH-036, image-freshness ORCH-058, staging-runner ORCH-123) исполняются host-side через ssh, docker CLI в контейнере нет, `docker.sock` сознательно не используется изнутри. Покрытие — `tests/test_orch123_staging_runner_exec.py` (TC-01 — обязательный регресс ORCH-116: КРАСНЫЙ до фикса / ЗЕЛЁНЫЙ после; TC-02…TC-14 + регресс R-2 «held не становится rollback») + зелёный анти-дрейф `tests/test_orch115_staging_runner.py` (TC-14: инварианты ORCH-115 целы; 3 теста обновлены под суперсед D4/D8/D1).
- **Детерминированный staging-раннер вместо LLM-деплойера на `deploy-staging`** (ORCH-115, `feat`): первый реализованный срез determinization-roadmap (ORCH-118 A6, `replace-deterministic-now`) — на стадии `deploy-staging` для self-hosting `orchestrator` **LLM-агент `deployer` заменён детерминированным кодом** (`src/staging_runner.py`). Работа агента на этой стадии была чисто механической (запуск staging-сюиты + маппинг exit-кода `staging_check.py``staging_status:`); каждый прогон жёг токены/время opus-агента (~40120k / 315 мин) и встраивал недетерминизм LLM в точку ветвления гейта. **Инвариант (NFR-1):** это замена *продюсера* артефакта, **не** гейта — контракт `15-staging-log.md`, гейт `check_staging_status`/`_parse_staging_status`, `STAGE_TRANSITIONS`, machine-verdict `staging_status:`, схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, скоуп self-hosting. ADR: `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`, сквозной `docs/architecture/adr/adr-0048-deterministic-staging-runner.md`.
- **Перехват в `launch_job` до `_spawn` (D1):** `if job.agent=="deployer" and staging_runner.should_intercept(job)``_run_staging_runner_job` (зеркало `_run_deploy_finalizer_job`, прецедент `deploy-finalizer`/`post-deploy-monitor` `launcher.py:389/394`): синхронно ведёт `jobs`-строку через `mark_job`, возвращает `None` (нет `agent_runs`-строки, нет токенов). Дискриминатор «staging vs prod» — **стадия задачи** `deploy-staging` (роль `deployer` общая для `deploy-staging`/`deploy`), не имя роли; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути). Для не-self репо `applies==False` → прод-`deployer` никогда не перехватывается.
- **Leaf `src/staging_runner.py` (новый, чистый never-raise):** по образцу `self_deploy`/`proc_group`/`staging_verdict` (на импорте только `config`/`proc_group`; `db`/`git_worktree`/`stage_engine`/`qg.checks`/`notifications` — лениво). Исполняет ту же сюиту (`docker exec orchestrator-staging python3 <repos_dir>/<self-repo>/scripts/staging_check.py --base-url http://localhost:<staging_port> --mode stub`, арги из config — ORCH-101, без host-хардкодов) через `proc_group.run_in_process_group` (tree-kill, таймаут `staging_runner_timeout_s=600`, малформ/непозитив → дефолт + WARNING); маппит exit-код **единым** контрактом `self_deploy.map_exit_code_to_status` (`0→SUCCESS`/иначе/None→`FAILED`, fail-closed; ORCH-061 infra-tolerance остаётся внутри `staging_check.py`, раннер не пересуживает — BR-4); пишет `15-staging-log.md` (тот же machine-key `staging_status:` UPPERCASE + 52c-схема, `author_agent: staging-runner`/`model_used: n/a`) + best-effort commit/push в **фичеветку** (не в `main` — гейт читает worktree первым, усиливает AC-8/BR-7); вызывает **существующий** `advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — без новых рёбер/исходов (transition-lease ORCH-114 берётся внутри `advance_stage`, раннер не трогает — граница O1).
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→advance (FAILED → тот же откат `deploy-staging → development` + developer-retry, что у FAILED-вердикта LLM, `stage_engine.py:932`); сюита **не исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл → bounded **DEFER** (re-queue `deployer`-джоба с задержкой + restart-safe маркер `staging-runner infra-retry` в `task_content`, счётчик подсчётом маркера — без правки схемы БД), на исчерпании `staging_runner_infra_max_retries=2` → fail-closed `FAILED` + advance + инфра-alert. Раннер **никогда** не делает тихий advance/ложный green, **никогда** не клинит очередь и **не** жжёт developer-retry на транзиентной инфре.
- **Self-hosting safety (BR-7/AC-8):** в командной строке раннера нет рестарта 8500 / `docker compose up orchestrator` / `--build` / force-push в `main` / правок `.env` — раннер только читает/исполняет staging-сюиту (8501) и пишет лог. Наблюдаемость (D10) — in-process счётчики (`runs`/`success`/`failed`/`tool_error`/`deferred`) + read-only блок `staging_runner` в `GET /queue` + один структурный лог-вердикт на прогон (различает код-фейл и tool-error). Флаги (`config.py`, дефолт = боевое): `staging_runner_enabled` (env `ORCH_STAGING_RUNNER_ENABLED`), `staging_runner_repos` (CSV; пусто → self-hosting only), `staging_runner_timeout_s`, `staging_runner_infra_max_retries`, `staging_runner_infra_retry_delay_s`. Откат = `ORCH_STAGING_RUNNER_ENABLED=false` → на `deploy-staging` снова LLM-`deployer` через `_spawn` **байт-в-байт**.
- **Норматив сопровождения ORCH-118 (NFR-6):** обновлены `docs/architecture/llm-call-sites.md` (A6 — реализован; машинный `ORCH-118-INVENTORY-BLOCK` сохраняет deployer как `avoidable=yes`/`axis=C` — LLM-ветвь жива как fallback), `llm-determinization-roadmap.md` (rank 1 deployer — ✅ реализован; машинный блок/инвариант «ровно один `first_slice = yes`» целы), `llm-usage-policy.md` (§5 — единственный транспорт S0 не нарушен, раннер LLM не зовёт), `.openclaw/agents/deployer.md` (LLM-ветвь `deploy-staging` — fallback), витрина `docs/overview/tech-pipeline.md`/`tech-agents.md`. Покрытие — `tests/test_orch115_staging_runner.py` (TC-01…TC-13) + зелёные `tests/test_llm_call_site_inventory.py`/`test_llm_determinization_docs.py` (TC-14).
- **Карта LLM-консультаций + control-path-ось «avoidable» + roadmap + нормативная политика** (ORCH-118, `docs`+`test`, inventory-first, docs+tests only): зонтичный follow-up RCA-трека ORCH-114/117 — у оркестратора не было ни нормативного критерия «где LLM нужен, а где это avoidable control path», ни карты мест вызова LLM, прибитой к коду. Выпущена **доказательная карта** каждого места, где control-path потребляет (или способен потребить) суждение LLM, с control-path-разметкой и классификацией; **упорядоченный roadmap** детерминированных замен; **нормативная политика** использования LLM; набор **структурных анти-дрейф тестов**. Это **docs + tests only**: `src/**`-рантайм не меняется → `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен **не** реализуются (FR-7); конкретные follow-up Plane-ID **не** фиксируются (R3/NFR-6 — кандидаты по роли). kill-switch не нужен (нет рантайм-поведения), как ORCH-077/079/101/102/103/011. ADR: `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`, сквозной `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`.
- **Единица инвентаря — LLM-консультация** (control-path потребляет суждение LLM), а **не** «спавн процесса / существование Claude CLI» (R4, capability ≠ consultation). Карта разводит **три ортогональных факта**: (1) consultation ≠ transport/slot (единственный транспорт LLM-консультации в `src/**``launcher._spawn`, `launcher.py:472`/CLI-сборка `610-614`; иного транспорта нет; job-роли `deploy-finalizer`/`post-deploy-monitor` занимают слот, но перехватываются в `launch_job` **до** `_spawn`, `launcher.py:389/394` — консультации нет); (2) **control-path (C) ≠ artifact-producer (P)** по коду-потребителю в `src/qg/checks.py` (C: гейт ветвится на LLM-вердикте; P: детерминированный гейт судит артефакт независимо — файлы/CI); (3) деривируемость вердикта из tool-сигналов.
- **Нормативное определение** «avoidable LLM control path» = двухбитный предикат (C-консультация **И** вердикт деривируем из exit-кодов `pytest`/smoke/`staging_check.py`/деплоя). Поимённый целевой набор (доказательно, прибит тестами): **avoidable = `{tester, deployer}`**; control-path-но-keep = `{reviewer}` (вердикт «приемлемость кода/решения» НЕ деривируем); не-control-path (P, keep) = `{analyst, architect, developer}`; уже детерминированы (эталон) = `{deploy-finalizer, post-deploy-monitor}`.
- **Документы (durable, `docs/architecture/`):** `llm-call-sites.md` (карта + машинно-читаемый блок инвентаря + control-path-разметка + классификация, снимок), `llm-determinization-roadmap.md` (порядок замен; рекомендованный первый срез — **deployer staging-status**, чистый маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C ORCH-036), `llm-usage-policy.md` (нормативный принцип + критерии keep/replace через ось + определение «avoidable»). Витрина `docs/overview/tech-quality-security.md` и `docs/architecture/README.md` ссылаются на карту и политику.
- **Анти-дрейф тесты (offline, без сети/LLM/subprocess-к-модели):** `tests/test_llm_call_site_inventory.py` (TC-01 единственный транспорт = `_spawn`; TC-12 отсутствие иного LLM-транспорта; TC-02 детерминированные модули без консультации; TC-03 промпты↔файлы; TC-04 тотальность+согласованность класса с осью; TC-05 keep-LLM именует суждение; TC-06 capability≠consultation; TC-09 снимок рантайм-контрактов; **TC-13** control-path-разметка сверена с потребителем в `src/qg/checks.py`; **TC-14** avoidable-набор = `{tester, deployer}`) и `tests/test_llm_determinization_docs.py` (TC-07 полнота roadmap+первый срез; TC-08 политика нормативна+определяет термин; TC-11 анти-фабрикация follow-up ID). Дискриминатор всех проверок — **«консультирует LLM», а не «спавнит subprocess»**. Норматив сопровождения: менял место вызова LLM или потребителя вердикта в `src/qg/checks.py` → обнови карту/разметку и политику в том же PR.
- **Sandbox-only fail-closed изоляция записи в Plane из тест-процесса** (ORCH-117, `fix`, bug→escalate full-cycle): закрыт корневой класс инцидента **ORCH-114** — тест/worktree-процесс выполнил РЕАЛЬНУЮ запись (`PATCH …/issues/… state=<Done>` + комментарий «Stage: deploy → done») против **боевого** Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен (`PLANE_HEADERS`/`PROJECT_ID` захвачены литералами **на импорте** — подмена env/токена постфактум бесполезна, NFR-4), и **ничто** не принуждало их писать только в sandbox. Симметрия прецеденту `tests/conftest.py::_no_telegram` (autouse-глушилка Telegram «pytest на проде слал реальные сообщения») — для Plane-**записи** такой защиты не было. Аддитивно, never-raise в боевом пути; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и имена `check_*`/machine-verdict-ключи/схема БД — **байт-в-байт не тронуты** (это изоляция клиента Plane, **не** Quality Gate и **не** стадия). Новый чистый leaf `src/plane_write_guard.py` (`decide(project_id, op, work_item) -> (ALLOW|BLOCK, reason)`, по образцу `deploy_status_guard`/`serial_gate`) врезан в **3 примитива записи** `plane_sync` (`update_issue_state`/`add_comment`/`_set_issue_state_direct`) **на момент вызова** — сразу после локального `_resolve_project_id` и **до** любого сетевого шага (ни GET, ни PATCH/POST). Гард активен **только в тест-процессе** (детект `"pytest" in sys.modules` / `PYTEST_CURRENT_TEST`); боевой и staging рантаймы (`uvicorn src.main:app`, без pytest в процессе) — строгий **no-op** (NFR-2/NFR-3). В тест-процессе запись разрешена **только** при одновременном (а) opt-in `plane_test_write_enabled=True` **и** (б) целевом проекте ∈ sandbox-allowlist `plane_test_sandbox_projects` (дефолт = единственный SANDBOX `8c5a3025-…`); иначе — default-deny; нерезолвимый проект → блок (fail-closed, NFR-1); боевой проект запрещён **даже при opt-in** (allowlist sandbox-only). Второй независимый sandbox-bound слой — autouse-floor `tests/conftest.py::_plane_sandbox_only` (opt-in OFF для всего сьюта, по образцу `_no_telegram`/`_disable_*`); sandbox-e2e ре-энейблит opt-in в своей фикстуре поверх floor. **Умышленно БЕЗ kill-switch прод-блока** (NFR-6/FR-7/anti-drift): выключателя, переоткрывающего прод-запись из pytest, нет — единственный реверс — sandbox-bound opt-in. Аудит: блок → громкий структурный ERROR (`project_id`/`work_item`/`op`/`reason` — делает инцидент класса ORCH-114 очевидным), разрешённая sandbox-запись → INFO. Новые ключи `ORCH_PLANE_TEST_WRITE_ENABLED` (дефолт `false`) / `ORCH_PLANE_TEST_SANDBOX_PROJECTS` (дефолт = SANDBOX id) с безопасными дефолтами; `scripts/staging_check.py` Block C (E2E в SANDBOX) — отдельный процесс с собственными httpx-вызовами, гардом не затронут. Покрытие — `tests/test_orch117_plane_write_isolation.py` (TC-01 — обязательный регресс ORCH-114: красный до врезки, зелёный после; TC-02…TC-14). ADR: `docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`, сквозной `docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md`.
- **Ownership-lease для side-effectful переходов стадий + умное восстановление при старте** (ORCH-114, `fix`, bug→escalate full-cycle): закрыт **корневой класс** инцидент-цепочки ORCH-110/111/112/113 — у side-effectful переходов стадий не было единого владения. `advance_stage` ре-ентерабельна и пишет стадию «голым» `UPDATE … WHERE id=?` (без compare-and-swap), а ≥5 акторов (монитор / Plane-webhook / reconciler F-1 / job-reaper / deploy-finalizer) входят в один переход независимо → конкурентный или после-рестартовый повторный вход **дважды** применял необратимые эффекты (merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя) и давал **противоречие rollback↔done** (инцидент ORCH-111, job 1914 / PR #130). Два комплементарных слоя, оба аддитивные, под единым kill-switch, never-raise: **(1) durable transition-lease** (новая таблица `transition_lease`) — владение на ВХОДЕ в side-effectful регион (второй актор, увидев живого владельца, не стартует тяжёлые под-гейты вовсе — предотвращение, не починка постфактум); **(2) expected-stage CAS** (`update_task_stage_cas`) — на ЗАПИСИ стадии (проигравший гонку — аборт без побочных эффектов), что закрывает и **6 путей записи стадии в обход `advance_stage`** (gitea×5 + plane rollback). Liveness владельца = `owner_pid` + `owner_boot_id` (НЕ heartbeat: блокирующий 900s merge re-test не может бить heartbeat — довод самого ORCH-113), что делает рестарт-recovery бесплатным (новый процесс → новый boot-id → все прежние lease мгновенно устаревшие → реклеймятся). Lease без собственного TTL: его потолок возраста = Tier-3 backstop `reaper_max_running_s` (5400) → сквозной бюджет ORCH-065/109/110/113 не тронут. `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / **схемы существующих таблиц** — байт-в-байт (одна аддитивная таблица, без epoch-колонки на `tasks`). Скоуп self-hosting (`transition_lease_repos=""` → только `orchestrator`; enduro не затронут); kill-switch `ORCH_TRANSITION_LEASE_ENABLED=false` → CAS вырождается в прежний безусловный `update_task_stage`, lease инертен → поведение байт-в-байт до ORCH-114. ADR: `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`, сквозной `docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`.
- **Leaf `src/transition_lease.py` (новый, чистый never-raise):** по образцу `serial_gate`/`coverage_gate`/`finalizer_liveness` (импортирует только `db`+`config`, лениво `merge_gate.pid_alive`/`qg.checks`/`notifications`; НЕ импортирует `stage_engine`/`launcher`) — `applies(repo)` / `acquire(task_id, owner, run_id, stage)` (атомарный rowcount-guard `INSERT … ON CONFLICT DO NOTHING` после очистки stale-строки) / `is_held_by_live_owner(task_id)` (fail-closed → defer на сомнении) / `release(task_id, force=False)` (holder-aware по boot) / `reclaim_if_stale` / `recover_on_startup` / `commit_stage_cas(task_id, expected, new, repo)` (flag-off → unconditional `update_task_stage`; flag-on → CAS) / `snapshot()`.
- **Интеграция:** `advance_stage` захватывает lease на входе в side-effectful ребро (`deploy-staging`/`deploy`), пишет стадию через CAS, освобождает lease в `try/finally` (на любом исходе, включая исключение/откат); **rollback-записи side-effectful под-гейтов** (`_handle_merge_gate_rollback`/`_handle_security_gate`/`_handle_coverage_gate`/`_handle_image_freshness`) пишут `development` через тот же CAS (общий хелпер `_rollback_stage_cas`, ADR-001 D4: защита rollback↔done — под держимым lease это единственный владелец, проигранный CAS → аборт без side-effects, не слепой перетир `done`); job-reaper `_finalizer_owns` обобщён с процесс-локального ORCH-113 (Tier-2/`deploy-staging`) на **durable cross-path** lease (defer при живом владельце; Tier-3 backstop игнорирует маркер → bounded reclaim; реап force-освобождает lease); reconciler F-1 и Plane-webhook (`_try_advance_stage`) делают **defer** при активном lease; `main.lifespan` зовёт `recover_on_startup()` после `requeue_running_jobs`. Наблюдаемость — read-only блок `transition_lease` в `GET /queue` + Telegram-алерт на форсированный/устаревший реклейм + опциональный `POST /transition-lease/release?work_item=<id>`. Покрытие — `tests/test_orch114_transition_ownership.py` (TC-01 обязательный регресс класса ORCH-111: красный до фикса, зелёный после; TC-02…TC-14 + регресс CAS на in-region rollback). Флаги (`config.py`, дефолт = боевое): `transition_lease_enabled` (env `ORCH_TRANSITION_LEASE_ENABLED`), `transition_lease_repos` (env `ORCH_TRANSITION_LEASE_REPOS`).
- **Гигиена shared deploy-базы: устойчивый self-deploy `git pull` к грязному дереву** (ORCH-112, `fix`, bug→escalate full-cycle): устранён инцидент ORCH-111 — self-deploy падал на шаге `git pull origin main` хост-хука с `error: Your local changes to the following files would be overwritten by merge: src/config.py` (грязь от неуспешной/отменённой/брошенной задачи ORCH-104 в общем main checkout) → деплой вставал → ручное вмешательство (на self-hosting — групповой риск). Решение — **resilient-pull, встроенный в прод-deploy-хук** (`--deploy`): перед `git pull` хук при обнаружении грязи приводит deploy-базу к чистому актуальному `origin/main` (`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`). Аддитивно, под kill-switch, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт не тронуты** (это устойчивость deploy-пути, **не** Quality Gate и **не** стадия). ADR: `docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`, сквозной `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`.
- **Leaf `src/checkout_hygiene.py` (новый, чистый never-raise):** по образцу `serial_gate`/`cancel`/`self_deploy` (импортирует только `config`, лениво `self_deploy`/`qg.checks`/`notifications`) — `applies(repo)` (kill-switch `checkout_hygiene_enabled` + скоуп `checkout_hygiene_repos`, пусто → self-hosting only, локально и ПЕРВЫМ), `hook_env(repo, work_item_id)` (env-префикс `CHECKOUT_HYGIENE=1 HYGIENE_REPORT=<host-path>`, инжектится в detached-команду хука только при `applies==True`, иначе `""` → голый pull 1:1), `read_report`/`alert_dirty` (наблюдаемость), `snapshot()` (read-only блок `GET /queue`).
- **Хук-блок «2a. Resilient pull» (`scripts/orchestrator-deploy-hook.sh`):** между шагом «1. Capture PREV_IMG» и «2. Pull», под `if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]`. **Сохранность (NFR-2, жёсткий контракт):** `git clean`**только `-fd`, НИКОГДА `-x`** (иначе удалил бы gitignored `.env`/прод-секреты, `data/*.db`/БД, `build/`); явные `-e '.deploy-prev-image-*'` и `-e 'deploy-hook.log'` (untracked-но-НЕ-ignored — иначе сломался бы rollback `do_rollback`); sibling `<repos_dir>/.deploy-state-*`/`.merge-lease-*.json` (под родителем `$REPO`) и `.git/worktrees/*` (внутри `.git/`) — вне области `git clean` в `$REPO`. Каждый git-шаг — `|| log "...continuing"` (never-break): сбой гигиены не ухудшает исход относительно текущего голого pull; на чистой базе блок — no-op (happy-path и exit-коды байт-в-байт). `--build-staging` (build из worktree, без pull) не затронут.
- **Сходимость после failed/cancelled (FR-2)** — этим же deploy-time self-heal (база сходится на следующем же self-deploy); `cancel_task` (ORCH-090) **не расширяется**, фоновый janitor **не вводится**. **Наблюдаемость (FR-4)** — хук пишет sentinel `hygiene` в deploy-state каталог; Phase-C finalizer (`stage_engine.run_deploy_finalizer`) читает (`read_report`) и шлёт Telegram-алерт (`alert_dirty`, кликабельный номер, best-effort, never-raise — сбой алерта не валит деплой).
- **Флаги** (`config.py`, дефолт = боевое): `checkout_hygiene_enabled` (env `ORCH_CHECKOUT_HYGIENE_ENABLED`), `checkout_hygiene_repos` (env `ORCH_CHECKOUT_HYGIENE_REPOS`). Откат = `ORCH_CHECKOUT_HYGIENE_ENABLED=false` → деплой байт-в-байт до ORCH-112. Покрытие — `tests/test_deploy_checkout_hygiene.py` (TC-01…TC-10: шелл-симуляция реального хука во временном git-репо без сети/прода/ssh + unit; TC-01 — обязательный регресс ORCH-111: КРАСНЫЙ до фикса, ЗЕЛЁНЫЙ после).
- **Job-reaper не реапит живой долго финализирующий монитор `deploy-staging`** (ORCH-113, `fix`, bug→escalate full-cycle): устранено расхождение состояния из инцидента ORCH-111 (deployer job 1914 / run_id 683). На ребре `deploy-staging → deploy` живой монитор (`launcher._monitor_agent`) штампит `agent_runs.finished_at`/`exit_code` **первым**, затем синхронно в своём потоке прогоняет тяжёлые edge-под-гейты (`security → merge-gate re-test → coverage → image-freshness`) — **минуты** — и лишь потом `_finalize_job`. Reaper Tier-2 меряет `finished_age_s` от `finished_at` (= начала финализации), поэтому по истечении `reaper_finalize_grace_s=300` трактовал живого долго финализирующего монитора как мёртвого и **независимо** повторял тот же тяжёлый advance: повторный re-test стал красным → ложный откат `deploy-staging → development` (+ ложный developer-retry) **параллельно** с тем, что исходный finalizer довёл deploy до SUCCESS и смержил PR — состояние раздвоилось. Аддитивно, под глобальным kill-switch, never-raise; `STAGE_TRANSITIONS`/`QG_CHECKS`/каждый `check_*`/machine-verdict ключи/схема БД — **байт-в-байт не тронуты**; `reaper_finalize_grace_s`/`reaper_max_running_s` и сквозной бюджет ORCH-065/109/110 (`5400 > Σ(gate-work)+grace`) сохранены; фикс не рестартит прод и не пушит `main`. ADR: `docs/work-items/ORCH-113/06-adr/ADR-001-reaper-finalizer-liveness-ownership.md`, сквозной `docs/architecture/adr/adr-0043-reaper-finalizer-liveness-ownership.md`.
- **Leaf `src/finalizer_liveness.py` (новый, процесс-локальный реестр владения):** чистый never-raise модуль (паттерн `serial_gate`/`coverage_gate`, без сети/БД) — `mark(job_id, run_id, stage)` / `clear(job_id)` / `is_active(job_id)` / `snapshot()`; состояние `{job_id: {...}}` под `threading.Lock`. Авторитетно in-memory, т.к. монитор и reaper — daemon-**потоки одного** uvicorn-процесса (CMD без `--workers`) с общей SQLite-БД. Собственного TTL нет — ограничение по времени даёт Tier-3 backstop. `is_active` при ошибке → `False` (консервативно: не блокировать добивание).
- **Эмиссия владения (`launcher._monitor_agent`):** `mark()` вызывается **сразу после** штампа `exit_code` (самый ранний момент Tier-2), хвост финализации вынесен в `_run_monitor_finalization` и обёрнут в `try/finally` с `clear()` в `finally` → исключение в потоке монитора гарантированно снимает владение, и реально мёртвый finalizer добивается. Маркер пишется **безусловно** (kill-switch гейтит только консультацию reaper, поэтому выключенный путь — байт-в-байт прежний). Хвост перенесён **дословно** (проверяется `git diff -w`: +49/0, нулевое изменение логики).
- **Консультация reaper (`job_reaper._reap_job` Tier-2):** при `reaper_finalizer_liveness_enabled` **И** стадии задачи `== "deploy-staging"` **И** активном владении → **defer** (счётчик + лог, не повторять advance), провал к Tier-3. **Tier-3 (`age >= reaper_max_running_s`) маркер игнорирует** — застрявший/мёртвый finalizer добивается в ограниченное время. Скоуп — только глобальный kill-switch `reaper_finalizer_liveness_enabled` (env `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`, дефолт `True`; `False` → reaper байт-в-байт прежний), **без** per-repo разреза (баг общий для всех репо со стадией `deploy-staging`).
- **Наблюдаемость:** аддитивные ключи `finalizer_liveness_enabled`/`finalizer_defers_total`/`finalizer_owned` в блоке `reaper` ответа `GET /queue` (существующие ключи не тронуты). Покрытие — `tests/test_orch113_reaper_finalizer_liveness.py` (TC-01…TC-08, включая обязательный регресс ORCH-111: КРАСНЫЙ до фикса, ЗЕЛЁНЫЙ после).
- **Merge-gate re-test: толерантность к инфра-таймауту + tree-kill спавненных pytest + контракт необходимости re-test** (ORCH-110, `fix`, bug→escalate full-cycle): устранён ложный откат `deploy-staging → development`, возникавший когда локальный re-test merge-gate падал по **таймауту** (инфра/ресурс) при зелёных CI + tester + staging (инцидент ORCH-109/PR #129: сюит 516.7s упёрся в бюджет 600s под CPU-голоданием от осиротевших pytest-процессов → `(False, "re-test timeout after 600s")``_handle_merge_gate_rollback` → каждый из 3 developer-retry падал так же → «Merge-gate still failing after 3 developer retries» → ручное вмешательство). Аддитивно, под 5 независимыми kill-switch, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика `check_*`/machine-verdict-ключи/схема БД — **байт-в-байт не тронуты**; INV-4 (никогда push/force-push `main`) и запрет рестарта прод-контейнера — соблюдены. ADR: `docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`, сквозной `docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md`.
- **D1 — process-group tree-kill (`src/proc_group.py`, новый stdlib-only leaf):** `merge_gate.retest_branch` и `coverage_gate.measure_coverage` теперь спавнят pytest в **отдельной группе процессов** (`start_new_session`) и при таймауте убивают **всё дерево** (`os.killpg`, каскад SIGTERM→grace→SIGKILL по образцу `launcher.stop_process`), а не только прямого потомка — осиротевшие внуки-pytest больше не переживают бюджет и не грузят CPU. Контракты возврата сохранены (меняется лишь побочный эффект — нет утечки). Грейс реюзит `agent_kill_grace_seconds`. Fallback never-break: `subprocess_tree_kill_enabled=False` или не-POSIX → прежний `subprocess.run(timeout=)`.
- **D2/D3 — классификация + маршрутизация инфра-таймаута:** чистый предикат `merge_gate.classify_retest_failure(reason)` различает `timeout`/`red`/`lock-busy`/`other` (scope-guard: `auto_rebase_onto_main`'s «rebase timeout» — НЕ инфра-таймаут re-test, остаётся на rollback-пути). Инфра-таймаут → новый `_handle_merge_gate_infra_retry` (ограниченный повтор по образцу `_handle_merge_gate_defer`: задача **остаётся на deploy-staging**, staging-deployer перезапускается с задержкой, **БЕЗ** отката на `development` и **БЕЗ** расхода developer-retry). Анти-над-толерантность (BR-6): детерминированно **красный** re-test / конфликт по-прежнему → `_handle_merge_gate_rollback`. Anti-loop: исчерпание бюджета → один **инфра-alert** (явно инфраструктурная формулировка «НЕ дефект кода» с кликабельным номером), задача НЕ уходит в `development`.

267
CLAUDE.md
View File

@@ -283,6 +283,273 @@ INV-4 (никогда push/force-push `main`) и запрет рестарта
`docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`, сквозной
`docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md`.
## Гигиена shared deploy-базы: устойчивый self-deploy `git pull` (ORCH-112)
Багфикс инцидента **ORCH-111** (bug → escalate full-cycle): прод-self-deploy падал на шаге
`git pull origin main` хост-хука (`scripts/orchestrator-deploy-hook.sh`) с `error: Your local changes
to the following files would be overwritten by merge: src/config.py` — грязь, оставленная
неуспешной/отменённой/брошенной задачей ORCH-104 в **общем** main checkout (`settings.deploy_host_repo_path`).
Деплой вставал → ручное вмешательство; на self-hosting (один прод-инстанс на все проекты) — групповой
риск. **Инвариант (нормативно):** shared main checkout `<host_repos_dir>/<repo>` — **deploy/worktree-management
база, НЕ редактируемый workspace** (агенты — worktree `git_worktree`, build — worktree-контекст, fallback'и
гейтов — read-only `git show origin/main`); локальных правок там быть не должно. Решение — **resilient-pull,
встроенный в хук** (`--deploy`): перед `git pull` хук при грязи приводит базу к чистому актуальному
`origin/main` (`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`). Аддитивно,
под kill-switch, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и
имена `check_*` / machine-verdict-ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт
не тронуты** (это устойчивость deploy-пути, **не** Quality Gate и **не** стадия).
- **Leaf `src/checkout_hygiene.py` (чистый never-raise):** по образцу `serial_gate`/`cancel`/`self_deploy`
(импортирует только `config`, лениво `self_deploy`/`qg.checks`/`notifications`) — `applies(repo)`
(kill-switch `checkout_hygiene_enabled` + скоуп `checkout_hygiene_repos`, **пусто → self-hosting only**,
локально и ПЕРВЫМ), `hook_env(repo, work_item_id)` (env-префикс `CHECKOUT_HYGIENE=1 HYGIENE_REPORT=<host-path>`,
инжектится в detached-команду `self_deploy.build_deploy_command` только при `applies==True`, иначе `""` →
хук видит `CHECKOUT_HYGIENE` неустановленным → голый `git pull` 1:1 до ORCH-112), `read_report`/`alert_dirty`
(наблюдаемость), `snapshot()` (read-only блок `GET /queue`).
- **Хук-блок «2a. Resilient pull»:** между шагом «1. Capture PREV_IMG» и «2. Pull», под
`if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]`. **Сохранность (NFR-2, жёсткий контракт):** `git clean` —
**только `-fd`, НИКОГДА `-x`** (иначе удалил бы gitignored `.env`/прод-секреты, `data/*.db`/БД, `build/`);
явные `-e '.deploy-prev-image-*'` и `-e 'deploy-hook.log'` (untracked-но-НЕ-ignored — иначе сломался бы
rollback `do_rollback`); sibling `<repos_dir>/.deploy-state-*`/`.merge-lease-*.json` и `.git/worktrees/*` —
вне области `git clean` в `$REPO`. Каждый git-шаг — `|| log "...continuing"` (never-break): сбой гигиены не
ухудшает исход относительно голого pull; чистая база → no-op (happy-path/exit-коды байт-в-байт);
`--build-staging` (build из worktree, без pull) не затронут.
- **Сходимость после failed/cancelled (FR-2)** — этим же deploy-time self-heal (база сходится на следующем же
self-deploy); `cancel_task` (ORCH-090) **не расширяется**, фоновый janitor **не вводится**.
**Наблюдаемость (FR-4)** — хук пишет sentinel `hygiene`; Phase-C finalizer (`stage_engine.run_deploy_finalizer`)
читает (`read_report`) и шлёт Telegram-алерт (`alert_dirty`, кликабельный номер, best-effort, never-raise).
- **Флаги** (`config.py`, дефолт = боевое): `checkout_hygiene_enabled` (env `ORCH_CHECKOUT_HYGIENE_ENABLED`),
`checkout_hygiene_repos` (env `ORCH_CHECKOUT_HYGIENE_REPOS`). Откат = `ORCH_CHECKOUT_HYGIENE_ENABLED=false` →
деплой байт-в-байт до ORCH-112. Покрытие — `tests/test_deploy_checkout_hygiene.py` (шелл-симуляция реального
хука во временном git-репо без сети/прода/ssh + unit; TC-01 — обязательный регресс ORCH-111: красный до
фикса, зелёный после). Детали — `docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`,
сквозной `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`.
## Единое владение side-effectful переходами: durable-lease + expected-stage CAS (ORCH-114)
Закрыт **корневой класс** инцидент-цепочки **ORCH-110/111/112/113**: у side-effectful переходов
стадий не было единого владения. `advance_stage` ре-ентерабельна и писала стадию «голым»
`UPDATE … WHERE id=?` (без compare-and-swap), а ≥5 акторов (монитор / Plane-webhook / reconciler
F-1 / job-reaper / deploy-finalizer) входят в один переход независимо → конкурентный или
после-рестартовый повторный вход **дважды** применял необратимые эффекты (`merge_pr` /
coverage-ratchet / image-rebuild / инициация прод-деплоя) и давал **противоречие rollback↔done**
(инцидент ORCH-111, job 1914 / PR #130). Это **обобщение** процесс-локальной finalizer-liveness
ORCH-113 в **durable cross-path** владение. Аддитивно, под единым kill-switch, never-raise; новый
leaf `src/transition_lease.py`. **Инвариант:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика
и имена `check_*` / machine-verdict-ключи / **схемы существующих таблиц** — байт-в-байт (одна
аддитивная таблица `transition_lease`, без epoch-колонки на `tasks`); hot-path `claim_next_job`
lease **не консультирует** (fail-open, очередь репо никогда не клинится).
- **Два комплементарных слоя (оба под `transition_lease_enabled`):** (1) **durable transition-lease**
(таблица `transition_lease`) — владение на ВХОДЕ в side-effectful регион: второй актор, увидев
живого владельца (`is_held_by_live_owner`), не стартует тяжёлые под-гейты вовсе (предотвращение,
не починка постфактум); (2) **expected-stage CAS** (`db.update_task_stage_cas` ↔
`commit_stage_cas`) — на ЗАПИСИ стадии: проигравший гонку аборт без побочных эффектов. CAS
закрывает и **6 путей записи стадии в обход `advance_stage`** (gitea×5 + plane rollback).
- **Liveness владельца = `owner_pid` + `owner_boot_id` (НЕ heartbeat):** блокирующий 900s merge
re-test не может бить heartbeat (довод самого ORCH-113) → рестарт-recovery бесплатен (новый
процесс → новый `boot_id` → все прежние lease мгновенно устаревшие → реклеймятся).
`main.lifespan` зовёт `recover_on_startup()` после `requeue_running_jobs`. Lease **без
собственного TTL**: его потолок возраста = Tier-3 backstop `reaper_max_running_s` (5400) →
сквозной бюджет ORCH-065/109/110/113 не тронут.
- **Интеграция:** `advance_stage` захватывает lease на входе в side-effectful ребро
(`deploy-staging`/`deploy`), пишет стадию через CAS, освобождает lease в `try/finally` (на
любом исходе, включая исключение/откат); job-reaper `_finalizer_owns` обобщён с процесс-локального
ORCH-113 на durable cross-path (defer при живом владельце; Tier-3 backstop игнорирует маркер →
bounded reclaim; реап force-освобождает lease); reconciler F-1 и Plane-webhook (`_try_advance_stage`)
делают **defer** при активном lease.
- **Флаги** (`config.py`, дефолт = боевое): `transition_lease_enabled` (env
`ORCH_TRANSITION_LEASE_ENABLED`; `False` → lease не пишется/не читается, CAS вырождается в прежний
безусловный `update_task_stage` → байт-в-байт до ORCH-114: reaper → ORCH-113 in-memory fallback,
reconciler/webhook skip-guard инертны), `transition_lease_repos` (env `ORCH_TRANSITION_LEASE_REPOS`;
CSV; **пусто → self-hosting only** — где живут необратимые рёбра; зеркало `coverage_gate_repos`,
enduro не затронут). Наблюдаемость — read-only блок `transition_lease` в `GET /queue` +
Telegram-алерт на форсированный/устаревший реклейм + опциональный
`POST /transition-lease/release?work_item=<id>`. Покрытие — `tests/test_orch114_transition_ownership.py`
(TC-01 обязательный регресс класса ORCH-111: красный до фикса, зелёный после; TC-02…TC-14). Детали —
`docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`, сквозной
`docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`.
## Sandbox-only fail-closed изоляция записи в Plane (ORCH-117)
Закрыт корневой класс инцидента **ORCH-114**: тест/worktree-процесс выполнил РЕАЛЬНУЮ запись
(`PATCH …/issues/… state=<Done>` + комментарий «Stage: deploy → done») против **боевого**
Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен
(`PLANE_HEADERS`/`PROJECT_ID` захвачены литералами **на импорте** `plane_sync` — подмена env/токена
постфактум бесполезна, NFR-4) и **ничто** не принуждало их писать только в sandbox. Прямой
прецедент — `tests/conftest.py::_no_telegram` (autouse-глушилка «pytest на проде слал реальные
Telegram-сообщения»); симметричной защиты для Plane-**записи** не было. **Инвариант:** запись в
боевой Plane-проект из любого pytest/worktree-процесса **физически невозможна** независимо от
токена. Аддитивно, never-raise в боевом пути; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и
имена `check_*`/machine-verdict-ключи/схема БД — **байт-в-байт не тронуты** (это изоляция клиента
Plane, **не** Quality Gate и **не** стадия).
- **Чокпоинт (D1):** новый чистый leaf `src/plane_write_guard.py` (`decide(project_id, op,
work_item) -> (ALLOW|BLOCK, reason)`, never-raise, по образцу `deploy_status_guard`/`serial_gate`/
`cancel`) врезан в **3 примитива записи** `plane_sync` (`update_issue_state` / `add_comment` /
`_set_issue_state_direct`) **на момент вызова** — сразу после локального `_resolve_project_id` и
**до** любого сетевого шага (ни GET, ни PATCH/POST). Все `set_issue_*`/`notify_*` сводятся к этим
трём примитивам → один гард ловит любой путь, включая будущие.
- **Детект тест-процесса (D2):** `"pytest" in sys.modules` `PYTEST_CURRENT_TEST` (на момент
вызова). Боевой и staging рантаймы — `uvicorn src.main:app`, pytest в процесс **не** импортируют →
гард там строгий **no-op** (NFR-2/NFR-3); worktree `python -m pytest` (инцидентный путь)
гарантированно имеет pytest в `sys.modules` → ловится.
- **Решение (D3):** default-deny. Запись из тест-процесса разрешена ⇔ одновременно (а) opt-in
`plane_test_write_enabled=True` **и** (б) целевой проект ∈ sandbox-allowlist
`plane_test_sandbox_projects` (дефолт = единственный SANDBOX `8c5a3025-4f9d-4190-b79f-fa06276bb27e`).
Нерезолвимый/пустой проект → блок (fail-closed, NFR-1). Боевой проект запрещён **даже при opt-in**
(allowlist sandbox-only). Внутренняя ошибка `decide` в тест-контексте → fail-CLOSED (`guard-error`).
- **Второй слой (D5):** независимый autouse-floor `tests/conftest.py::_plane_sandbox_only` форсит
opt-in OFF для **всего** сьюта (по образцу `_no_telegram`/`_disable_*`); sandbox-e2e ре-энейблит
opt-in в своей фикстуре поверх floor. Два sandbox-bound слоя → нет одиночной точки, чьё выключение
переоткрывает прод.
- **Умышленно БЕЗ kill-switch прод-блока (D4, NFR-6/FR-7, anti-drift):** выключателя,
переоткрывающего прод-запись из pytest, **нет** — единственный реверс — sandbox-bound opt-in. Не
добавлять «общий kill-switch гарда» (реинтродуцирует дефект ORCH-114; reviewer ловит как ≥P1).
- **Аудит (D7):** блок → громкий структурный ERROR (`project_id`/`work_item`/`op`/`reason`:
`prod-project-in-test`/`opt-in-disabled`/`ambiguous-target`/`guard-error`); разрешённая
sandbox-запись → INFO. **Флаги** (`config.py`, дефолты безопасные): `plane_test_write_enabled`
(env `ORCH_PLANE_TEST_WRITE_ENABLED`, дефолт `False`), `plane_test_sandbox_projects` (env
`ORCH_PLANE_TEST_SANDBOX_PROJECTS`, CSV). `scripts/staging_check.py` Block C (E2E в SANDBOX) —
отдельный процесс с собственными httpx-вызовами, гардом не затронут. Покрытие —
`tests/test_orch117_plane_write_isolation.py` (TC-01 — обязательный регресс ORCH-114: красный до
врезки, зелёный после; TC-02…TC-14). Детали —
`docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`, сквозной
`docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md`.
## Детерминированный staging-раннер вместо LLM-деплойера (ORCH-115)
Первый реализованный срез determinization-roadmap (ORCH-118 A6, `replace-deterministic-now`): на
стадии `deploy-staging` для self-hosting `orchestrator` **LLM-агент `deployer` заменён
детерминированным кодом** (`src/staging_runner.py`). Работа агента на этой стадии была чисто
механической (запуск staging-сюиты + маппинг exit-кода) — теперь её делает leaf, перехватываемый в
`launch_job` **до `_spawn`** (прецедент `deploy-finalizer`/`post-deploy-monitor`,
`launcher.py:389/394`). **Инвариант (NFR-1):** замена *продюсера* артефакта, **не** гейта —
контракт `15-staging-log.md`, гейт/`_parse_staging_status`/имя `check_staging_status`,
`STAGE_TRANSITIONS`, machine-verdict `staging_status:`, схема БД — **байт-в-байт не тронуты**.
Аддитивно, под kill-switch, never-raise, fail-closed.
- **Перехват (D1):** `launch_job` — `if job.agent=="deployer" and staging_runner.should_intercept(job)`
→ `_run_staging_runner_job` (зеркало `_run_deploy_finalizer_job`): синхронно ведёт `jobs`-строку
через `mark_job`, возвращает `None` (нет `agent_runs`). Дискриминатор «staging vs prod» —
**стадия задачи** `deploy-staging` (роль `deployer` общая для `deploy-staging`/`deploy`), не имя
роли; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Раннер (D2-D7):** leaf по образцу `self_deploy`/`proc_group`/`staging_verdict` (на импорте только
`config`/`proc_group`; `db`/`git_worktree`/`stage_engine`/`qg.checks`/`notifications` — лениво).
Исполняет ту же сюиту (`docker exec orchestrator-staging python3 .../staging_check.py --base-url
http://localhost:8501 --mode stub`, арги из config — ORCH-101) через `proc_group` (tree-kill,
таймаут `staging_runner_timeout_s=600`); маппит exit-код **единым** контрактом
`self_deploy.map_exit_code_to_status` (`0→SUCCESS`/иначе/None→`FAILED`; ORCH-061 infra-tolerance
остаётся внутри `staging_check.py`, раннер не пересуживает); пишет `15-staging-log.md`
(`author_agent: staging-runner`/`model_used: n/a`, 52c-схема) + best-effort push в **фичеветку**
(не в `main` — гейт читает worktree первым, усиливает AC-8); вызывает **существующий**
`advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — без новых
рёбер/исходов (lease ORCH-114 берётся внутри `advance_stage`, раннер не трогает).
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→
advance (FAILED → тот же откат `deploy-staging → development` + developer-retry, что у LLM); сюита
**не исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл →
bounded **DEFER** (re-queue `deployer`-джоба + restart-safe маркер `staging-runner infra-retry` в
`task_content`, без правки схемы БД), на исчерпании `staging_runner_infra_max_retries` → fail-closed
`FAILED` + advance + alert. Никогда тихий advance/ложный green; не клинит очередь; не жжёт
developer-retry на транзиентной инфре.
- **Флаги** (`config.py`, дефолт = боевое): `staging_runner_enabled` (kill-switch, env
`ORCH_STAGING_RUNNER_ENABLED`), `staging_runner_repos` (CSV; **пусто → self-hosting only**),
`staging_runner_timeout_s=600`, `staging_runner_infra_max_retries=2`,
`staging_runner_infra_retry_delay_s=30`. Откат = `ORCH_STAGING_RUNNER_ENABLED=false` → на
`deploy-staging` снова LLM-`deployer` через `_spawn` **байт-в-байт**. Наблюдаемость — in-process
счётчики + read-only блок `staging_runner` в `GET /queue` + один структурный лог-вердикт на прогон
(различает код-фейл и tool-error). Норматив сопровождения ORCH-118: обновлены
`llm-call-sites.md`/`llm-determinization-roadmap.md`/`llm-usage-policy.md` (A6 — реализован, машинные
блоки/инвариант «единственный транспорт S0» целы) + `deployer.md` (LLM-ветвь — fallback). Покрытие —
`tests/test_orch115_staging_runner.py` (TC-01…TC-13) + зелёные LLM-анти-дрейф тесты (TC-14). Детали —
`docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`, сквозной
`docs/architecture/adr/adr-0048-deterministic-staging-runner.md`.
- **ORCH-123 (host-side исполнение + классификация environment-дефекта, bug→escalate full-cycle,
adr-0049):** фикс инцидента **ORCH-116** — раннер ORCH-115 вызывал `docker exec` **изнутри**
прод-контейнера, где **нет docker CLI** (`Dockerfile:11` несёт только `openssh-client git curl`;
`docker.sock` смонтирован, клиента нет) → `FileNotFoundError` → постоянный environment-дефект
ложно маршрутизировался как код-фейл-откат `deploy-staging → development` (с расходом
developer-retry); любая self-hosting задача на `deploy-staging` была обречена. Аддитивно, под
флагами, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и имена
`check_staging_status`/`_parse_staging_status`/machine-verdict-ключи/схема БД — **байт-в-байт не
тронуты** (замена *стратегии исполнения продюсера*, **не** гейта; зеркало ORCH-115 NFR-1).
**(D1)** `build_staging_command` обёртывает ту же `docker exec orchestrator-staging … staging_check.py
… --mode stub` в `ssh -o StrictHostKeyChecking=no <deploy_ssh_user>@<deploy_ssh_host> '<…>'`
(зеркало `self_deploy`/`image_freshness`; канал доверенный `ORCH_DEPLOY_SSH_HOST=127.0.0.1`,
ssh-ключ `:ro`, `openssh-client` в образе — новых секретов/привилегий нет; сюита по-прежнему бежит
**внутри** `orchestrator-staging` 8501, меняется лишь инициатор `docker exec`). **(D2 security)**
docker CLI/SDK в контейнер **не** добавляется, `docker.sock` **не** используется изнутри (это
root-эквивалентное расширение поверхности атаки, доступное и LLM-агентам). **(D3)** двухуровневый
исход ORCH-115 заменён **трёхсторонней** чистой `classify_staging_outcome` (зеркало
`merge_gate.classify_retest_failure`): `suite-ran` (распознанный exit-код ≠255 **без** env-маркера в
stderr → доверяем коду `0→SUCCESS`/`≠0→FAILED`; анти-over-tolerance BR-3), `permanent-env`
(spawn-error `rc=None`/нет ssh-target/`rc∈{126,127}`/env-маркер `No such container`/`Cannot connect
to the Docker daemon` → ретрай бессмыслен), `transient-infra` (timeout/ssh `rc=255`/неизвестно →
ретрай осмыслен); дизамбигуация `exit=1`-коллизии — скан stderr на env-маркеры, не голый код;
fail-safe → `transient-infra`. **(D4 инвариант «инфра ≠ код-фейл»)** `permanent-env` → немедленный
**infra-HOLD** (DEFER пропускается, `15-staging-log.md` НЕ пишется, advance НЕ зовётся, developer-retry
НЕ жжётся, alert «НЕ дефект кода»); `transient-infra` → bounded DEFER, на исчерпании — тоже
infra-HOLD+alert (**супершид ORCH-115 D5** fail-closed-FAILED-отката). «Сюита **не** исполнилась»
(env ИЛИ инфра) **никогда** не оканчивается код-фейл-откатом — закрывает RCA-класс ORCH-110 на
staging-ребре; задача держится на `deploy-staging` (reconciler `advance_if_gate_passed` на red-гейте
→ `None`, без отката, R-2). **(D5)** `preflight()` пробит host-side канал на старте `main.lifespan`
(best-effort, never-blocks). **(D6)** новый флаг `staging_runner_exec_host_side=True`
(env `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE`); откат — `=false` (in-container 1:1) или
`ORCH_STAGING_RUNNER_ENABLED=false` (LLM-deployer 1:1). **(D8)** счётчик `permanent_env` + `exec_host_side`
+ `preflight` в блоке `staging_runner` `GET /queue`. Покрытие — `tests/test_orch123_staging_runner_exec.py`
(TC-01 обязательный регресс ORCH-116 red→green; TC-02…TC-14 + R-2) + зелёный анти-дрейф ORCH-115.
Детали — `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`,
сквозной `docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`.
## Детерминированный test-раннер вместо LLM-тестера (ORCH-116)
Второй реализованный срез determinization-roadmap (ORCH-118 A5, `needs-hybrid-fallback`): на стадии
`testing` для self-hosting `orchestrator` **LLM-агент `tester` заменён детерминированным кодом**
(`src/test_runner.py`). PASS/FAIL-ядро было деривируемо (регресс `pytest` + read-only smoke) — теперь
его делает leaf, перехватываемый в `launch_job` **до `_spawn`** (прецедент `deploy-finalizer`/
`post-deploy-monitor`/`staging-runner`, `launcher.py:397/402/405`). **Инвариант (NFR-1):** замена
*продюсера* артефакта, **не** гейта — контракт `13-test-report.md`, гейт/`_parse_tests_verdict`/имя
`check_tests_passed`, `STAGE_TRANSITIONS`, machine-verdict `result:` (+ legacy `verdict:`/`status:`),
схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, гибрид
(LLM строго off-control-path).
- **Перехват (D1):** `launch_job` — `if job.agent=="tester" and test_runner.should_intercept(job)`
→ `_run_test_runner_job` (зеркало `_run_staging_runner_job`): синхронно ведёт `jobs`-строку через
`mark_job`, возвращает `None` (нет `agent_runs`). Дискриминатор — роль `tester` **И** стадия задачи
`testing` (defense-in-depth: `tester` — единственный агент входа в `testing`, коллизии стадий нет, в
отличие от общей роли `deployer`/ORCH-115) **И** `applies(repo)`; `should_intercept` never-raise →
`False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Раннер (D2-D7):** leaf по образцу `staging_runner`/`self_deploy`/`proc_group` (на импорте только
`config`/`proc_group`; `db`/`git_worktree`/`self_deploy`/`qg.checks`/`stage_engine`/`notifications`
— лениво). `applies` = kill-switch `test_runner_enabled` + скоуп `test_runner_repos` (пусто →
self-hosting only) **И** резолв тест-контракта `_has_test_contract` (BR-9: репо без контракта →
LLM-tester, enduro-trails 1:1 даже если руками в CSV). Исполняет `python -m pytest
<test_runner_target>` **в worktree ветки** (анти checkout-гонка) через `proc_group`
(tree-kill, таймаут `test_runner_timeout_s=900`) + опц. read-only smoke (`/health`/`/status`/`/queue`
+ блок `serial_gate`, stdlib `urllib`; транзиент → ограниченный ретрай, не-200/нет блока → немедленный
FAIL; `test_runner_smoke_enabled`); маппит exit-код **единым** `self_deploy.map_exit_code_to_status`
в токенах `result:` (`0→PASS`/иначе/None→`FAIL`, fail-closed; smoke-провал AND-ится в `FAIL`); пишет
`13-test-report.md` (`author_agent: test-runner`/`model_used: n/a`, 52c-схема) + best-effort push в
фичеветку; вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")`
— без новых рёбер/исходов (lease ORCH-114 берётся внутри `advance_stage`).
- **Анти-коллизия 52c-`status:` ↔ парсер (D6.1, специфично для tester):** `_parse_tests_verdict` читает
вердикт из **трёх** равноранговых полей (`verdict:`/`status:`/`result:`) с negative-token-priority →
52c-обязательное `status:` раннера **ВСЕГДА выровнено** по вердикту (`PASS → status: success`, `FAIL
→ status: failed`), иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL. Прибито
unit-тестом через неизменённый парсер.
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→
advance (FAIL → тот же откат `testing → development` + developer-retry, что у LLM); сюита **не
исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл → bounded
**DEFER** (re-queue **`tester`**-джоба + restart-safe маркер `test-runner infra-retry` в
`task_content`, без правки схемы БД), на исчерпании `test_runner_infra_max_retries` → fail-closed
`FAIL` + advance + alert. Никогда тихий advance/ложный green; не клинит очередь; не жжёт
developer-retry на транзиентной инфре.
- **Флаги** (`config.py`, дефолт = боевое): `test_runner_enabled` (kill-switch, env
`ORCH_TEST_RUNNER_ENABLED`), `test_runner_repos` (CSV; **пусто → self-hosting only**),
`test_runner_target=tests/`, `test_runner_timeout_s=900`, `test_runner_smoke_enabled`,
`test_runner_infra_max_retries=2`, `test_runner_infra_retry_delay_s=30`. Откат =
`ORCH_TEST_RUNNER_ENABLED=false` → на `testing` снова LLM-`tester` через `_spawn` **байт-в-байт**.
Наблюдаемость — in-process счётчики + read-only блок `test_runner` в `GET /queue` + один структурный
лог-вердикт на прогон (различает код-фейл и tool-error). **Гибрид (BR-8/NFR-7):** LLM строго
off-control-path — детерминированный раннер единственный продюсер `result:`; будущий триаж падений не
выносит/не переопределяет вердикт и не добавляет ребро в `STAGE_TRANSITIONS` (Phase 1 не реализован).
Норматив сопровождения ORCH-118: обновлены `llm-call-sites.md`/`llm-determinization-roadmap.md`/
`llm-usage-policy.md` (A5/rank 2 — реализован, машинные блоки/инвариант «ровно один `first_slice
целы) + `tester.md` (LLM-ветвь — fallback). Покрытие — `tests/test_orch116_test_runner.py`
(TC-01…TC-14) + зелёные LLM-анти-дрейф тесты (TC-15). Детали —
`docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной
`docs/architecture/adr/adr-0050-deterministic-test-runner.md`.
## Машинный журнал уроков (ORCH-098)
Шаг 1 («Фундамент», F2) эпика саморазвития: формализует свободнотекстовые «уроки» из `memory/` в
**машинную структурированную таблицу отклонений конвейера** `lessons`, фундамент для будущих

View File

@@ -149,6 +149,12 @@ uvicorn src.main:app --reload --port 8500
| `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_ESTIMATOR_ENABLED` | Kill-switch оценки задачи (ORCH-020): Plane-статус «Оценка» прогнозирует стоимость/время/токены/story-points по истории; `false` → статус не обрабатывается, ничего не пишется (нулевая регрессия) | `true` |
| `ORCH_ESTIMATOR_REPOS` | CSV область репо для оценки; **пусто → self-hosting only** (`orchestrator`) — enduro не затронут (ORCH-020) | `""` |
| `ORCH_ESTIMATOR_MIN_SAMPLES` | Порог истории, ниже которого подмешивается bootstrap-дефолт прогноза (ORCH-020) | `3` |
| `ORCH_ESTIMATOR_BOOTSTRAP_TOKENS` / `_COST_USD` / `_SECONDS` | Bootstrap-прогноз при пустой истории (токены/стоимость/время; ORCH-020) | `2000000`/`3.0`/`1800` |
| `ORCH_ESTIMATOR_SP_COST_THRESHOLDS` | 4 возрастающих кат-оффа стоимости (t1,t2,t3,t5) для бакета story-points (`<=t1→1``<=t5→5`, иначе `8`; ORCH-020) | `0.50,2.00,5.00,12.00` |
| `ORCH_ESTIMATOR_WALL_CAP_S` / `_MAX_INFLIGHT` | Отсечка аномального wall-времени в истории / опц. семафор сглаживания массовой нагрузки (ORCH-020) | `86400`/`64` |
| `ORCH_AGENT_HOME_DIR` | ORCH-101: HOME акторских процессов + таргет маунтов `.claude`/`.ssh` + `ARG APP_HOME` (группа ORCH-040) | `/home/slin` |
| `ORCH_AGENT_GIT_NAME` / `ORCH_GIT_EMAIL_DOMAIN` | ORCH-101: git-идентичность коммитов агентов (`claude-bot@mva154.local` при дефолтах) | `claude-bot` / `mva154.local` |
| `ORCH_STAGING_PORT` | ORCH-101: порт staging (читают `image_freshness` и compose); guard fail-closed при совпадении с прод-портом (ORCH-058 AC-9) | `8501` |

View File

@@ -47,6 +47,7 @@ check_tests_passed → check_staging_status → check_deploy_status`.
|----------|----------------|-----------|------------------|--------------------------|-------------------------|
| `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`) | — |
| `01-questions.md` | analyst | when-applicable | `analysis` | **сигнальный** (гейтом НЕ парсится); механизм — ветка Needs Input в `_handle_analysis_approved_flow` (ORCH-120, adr-0053): активные блокирующие вопросы → `set_issue_needs_input` (приоритет над «файлы готовы») | — (не machine-verdict) |
| `02-trz.md` | analyst | required | `analysis` | то же | — |
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
@@ -72,6 +73,10 @@ check_tests_passed → check_staging_status → check_deploy_status`.
- **Категория `when-applicable`** = документ пишется при наличии соответствующего предмета
(инфра / данные / security / post-deploy). Его отсутствие — не нарушение приёмки.
- **`05-…` / `09-…` / `11-…`** — зарезервированные/legacy номера, в текущем каноне не используются.
- **Префикс `01-` (DQ-4 ORCH-120)** — общий для артефактов стадии `analysis` владельца `analyst`:
`01-brd.md` — обязательный deliverable (гейтится `check_analysis_complete`), `01-questions.md`
**сигнальный** when-applicable артефакт того же владельца/стадии. Коллизии нет: файлы разноимённые,
`check_analysis_complete` проверяет ровно `01-brd.md`/`02`/`03`/`04` (`01-questions.md` им не парсится).
---

43
docs/_templates/01-questions.md vendored Normal file
View File

@@ -0,0 +1,43 @@
---
work_item: ORCH-NNN
stage: analysis
author_agent: analyst
status: needs-input
created_at: <YYYY-MM-DD>
model_used: <resolve ORCH-41>
---
# 01 — Открытые вопросы (Open Questions): ORCH-NNN — <название>
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
> **Сигнальный** when-applicable артефакт (ORCH-120, adr-0053). Пишется аналитиком через **Write
> tool** ТОЛЬКО при **блокирующей** неоднозначности бизнес-запроса, когда выпустить корректные 4
> deliverables нельзя без ответа заказчика. Наличие этого файла с **активными** вопросами уводит
> задачу в **Needs Input** (приоритет над «файлы готовы»). **Не** machine-verdict: гейтом
> (`check_analysis_complete`/`check_analysis_approved`) НЕ парсится — это сигнал движку
> (`_handle_analysis_approved_flow`).
>
> ⚠️ Если блокирующих вопросов НЕТ — **не создавай** этот файл; выпускай полный пакет (`01-brd.md`/
> `02-trz.md`/`03-acceptance-criteria.md`/`04-test-plan.yaml`). Не фабрикуй требования ради сдачи 4
> файлов.
## 1. Контекст
<Что именно в бизнес-запросе (`00-business-request.md`) блокирует выпуск корректного пакета. Какие
факты установлены, а какие — нет. На какой код `src/` это влияет.>
## 2. Блокирующие вопросы
> Каждый вопрос — конкретный, отвечаемый, с вариантами (где уместно) и указанием, почему ответ
> блокирует анализ. Нумеруй (Q-1, Q-2, …).
- **Q-1** — <вопрос>
- Вариант A: <…> (последствие)
- Вариант B: <…> (последствие)
- Почему блокирует: <без ответа нельзя выпустить BR/TRZ, т.к. …>
- **Q-2** — …
## 3. Что разблокирует анализ
<Какие ответы переводят задачу из Needs Input обратно в работу: после ответов заказчика в Plane
аналитик перезапускается (resume), читает свежие комментарии и выпускает полный пакет. Если часть
вопросов снята, а часть осталась — **перепиши** этот файл (оставь только актуальные блокеры), иначе
выпусти 4 deliverables (свежий пакет supersedeит этот файл по mtime, DQ-2).>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0043: Reaper Tier-2 — in-memory ownership-маркер финализации `deploy-staging` (живой finalizer не реапится)
- **Статус:** proposed
- **Дата:** 2026-06-15
- **Задача:** ORCH-113 (bug → escalate full-cycle; кластер инцидента ORCH-111)
- **Детальный ADR:** `docs/work-items/ORCH-113/06-adr/ADR-001-reaper-finalizer-liveness-ownership.md`
- **Уточняет:** `adr-0011` (job-reaper/lease-reclaim ORCH-065), `adr-0040` (timeout-бюджеты ORCH-109),
`adr-0042` (merge-gate re-test infra-tolerance + tree-kill ORCH-110), `adr-0041`
(ORCH-111 `proc_blocking` — комплементарный наблюдатель того же инцидента)
## Контекст
На ребре `deploy-staging → deploy` живой монитор (`launcher._monitor_agent`) штампит
`agent_runs.finished_at`/`exit_code` **первым**, затем синхронно, в своём потоке, прогоняет тяжёлый
набор edge-под-гейтов через `_try_advance_stage → advance_stage` (`stage_engine.py:327368`):
`security``merge-gate` (полный локальный re-test, `merge_retest_timeout_s=900`) → `coverage`
(`pytest --cov`) → `image-freshness` (docker-rebuild + пересоздание staging) — **минуты**, — и лишь
потом `_finalize_job`. Reaper Tier-2 (`job_reaper.py:197209`) меряет `finished_age_s` от
`finished_at` = **начала** финализации и по `reaper_finalize_grace_s=300` считает живого, долго
финализирующего монитора мёртвым → независимо повторяет тот же тяжёлый advance. Атомарный
claim-before-act защищает лишь **флип строки** job, но не **side-effectful исполнение edge-гейтов**
(монитор не claim'ит строку перед `advance_stage`) → две `advance_stage` параллельно.
Инцидент ORCH-111 (job 1914): повторный re-test красный, ложный откат `deploy-staging → development`
(+ ложный developer-retry), **параллельно** исходный finalizer довёл deploy до SUCCESS и смержил
PR #130 — состояние раздвоилось. Реального сигнала «жив ли finalizer» нет (pid агента в Tier-2 мёртв в
обоих случаях). Per-stage grace, покрывающая Σ финализации (≈4160с), невозможна без нарушения сквозного
бюджета ORCH-065/109/110 `reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace (≈4460)`.
**Решающий факт (проверен):** монитор и reaper — daemon-**потоки одного** uvicorn-процесса (CMD без
`--workers`), общая SQLite-БД → живость finalizer'а определяется **in-memory**. Рестарт покрыт
существующим `requeue_running_jobs()` (running→queued), вызываемым в `main.lifespan` **до** старта reaper.
## Решение
1. **Leaf `src/finalizer_liveness.py`** — чистый процесс-локальный реестр владения финализацией
(паттерн `serial_gate`/`coverage_gate`: never-raise, без сети/БД): `mark(job_id, run_id, stage)` /
`clear(job_id)` / `is_active(job_id) -> bool` / `snapshot()`; `{job_id: {...}}` + `threading.Lock`;
собственного TTL нет (ограничение по времени даёт Tier-3).
2. **Эмиссия владения**`launcher._monitor_agent`: `mark(...)` сразу после штампа `exit_code`
(самый ранний момент Tier-2), `clear(...)` в `try/finally` вокруг хвоста финализации → исключение
в потоке монитора гарантированно снимает владение (reaper добивает). Гибель процесса → рестарт →
`requeue_running_jobs` → реестр пуст (restart-safe без durable-хранения).
3. **Консультация reaper**`_reap_job` Tier-2 (`exit_code` записан, `finished_age >= grace`): если
`reaper_finalizer_liveness_enabled` **И** стадия `== "deploy-staging"` **И** `is_active(job_id)`
**defer** (лог + счётчик), не реапить через Tier-2, провалиться к Tier-3. Иначе — прежний путь.
**Tier-3 (`age >= reaper_max_running_s`) маркер игнорирует** — добивает всегда в ограниченное время.
4. **Скоуп/флаг** — только глобальный kill-switch `reaper_finalizer_liveness_enabled`
(env `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`, дефолт `True`); **без** per-repo разреза (баг общий
для всех репо со стадией `deploy-staging`; per-repo оставил бы баг активным для части репо).
`False` → reaper байт-в-байт прежний; стадии `!= deploy-staging` не консультируются.
5. **Наблюдаемость** — счётчик `finalizer_defers_total` + размер `snapshot()` в блоке `reaper`
`GET /queue`; существующие ключи ответа не меняются; новых эндпоинтов нет.
**Инварианты:** `STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict ключи / схема
существующих таблиц — **байт-в-байт**; **нулевое** изменение схемы БД; reaper остаётся never-raise
наблюдателем; `reaper_finalize_grace_s` и `reaper_max_running_s` **не меняются** (сквозной бюджет цел);
фикс не рестартит прод и не пушит `main`.
## Альтернативы
- Per-stage grace, покрывающая Σ — отвергнуто (нарушает бюджет `5400 > Σ+grace`; таймер = источник бага).
- Durable-колонка (heartbeat/owner-токен) — отвергнуто (один процесс → in-memory авторитетно; рестарт
покрыт requeue; блокирующий re-test не может бить heartbeat).
- Sub-state `finalizing` в `jobs.status` — отвергнуто (меняет семантику статуса для
claim/requeue/reconciler/reaper — нарушение NFR-2).
- Lease-файл на `(job, stage)` — отвергнуто (тяжелее, дублирует merge-lease, TTL = таймер-проблема).
- Флип job из `running` до тяжёлых гейтов — отвергнуто (ломает `get_running_jobs`/метрики и
restart-requeue).
## Последствия
- (+) Устранены повторный прогон edge-гейтов, ложный откат и расхождение состояния при живом долгом
finalizer'е `deploy-staging`; идемпотентность исполнения edge-гейтов через владение.
- (+) Реально мёртвый/застрявший finalizer добивается (finally-clear → Tier-2; иначе Tier-3); функция
reaper ORCH-065 сохранена.
- (+) Нулевое изменение схемы и контрактов; сквозной бюджет ORCH-065/109/110 не тронут; откат — один
env-флаг.
- () Гарантия владения валидна при **одном процессе/одной БД** (проверено: один uvicorn-воркер); ввод
`--workers>1` потребует durable-сигнала (риск в work-item 10-tech-risks).
- () Окно «штамп `finished_at``mark()`» (git push) маркером не покрыто — закрыто прежним grace=300.
## Связи
- Базируется/уточняет: `adr-0011`, `adr-0040`, `adr-0042`, `adr-0041`.
- Союзные задачи кластера инцидента ORCH-111: `ORCH-110` (инфра-толерантность merge-gate — отдельный
объём, не дублировать), `ORCH-109` (бюджеты).
- Детально: `docs/work-items/ORCH-113/06-adr/ADR-001-reaper-finalizer-liveness-ownership.md`.
</content>

View File

@@ -0,0 +1,66 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0044: Гигиена shared deploy-базы — устойчивый self-deploy `git pull`
Сквозное (cross-cutting) решение. Детальный per-work-item ADR —
`docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`.
## Статус
Proposed (ORCH-112)
## Контекст (сквозной)
Глобальный путь прод-деплоя self-hosting (`deploy`-стадия, ORCH-036) исполняет хост-хук
`scripts/orchestrator-deploy-hook.sh`, чей шаг «2. Pull latest code» — **голый** `git pull origin main`
в shared main clone (`settings.deploy_host_repo_path`). Любая грязь рабочего дерева (модифицированный
tracked-файл и/или untracked-остатки failed/cancelled/брошенной задачи) **блокирует** merge → деплой
встаёт → ручное вмешательство. На self-hosting (один прод-инстанс на все проекты с общей БД/очередью)
это **групповой риск**: залипший self-deploy орка останавливает обслуживание всех проектов
(инцидент ORCH-111, грязь от ORCH-104).
## Решение (сквозное)
Вводится **resilient-pull, встроенный в прод-deploy-хук** (`--deploy`), + новый чистый never-raise
leaf-компонент `src/checkout_hygiene.py`:
- **Хук** перед `git pull origin main` приводит грязную deploy-базу к чистому актуальному `origin/main`
(`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`), **строго сохраняя**
rollback/лог-артефакты. Гейт — env `CHECKOUT_HYGIENE`, инжектится `self_deploy.build_deploy_command`.
- **Leaf** `checkout_hygiene` решает условность (`applies(repo)`: kill-switch `checkout_hygiene_enabled`
+ скоуп `checkout_hygiene_repos`, пусто → self-hosting only), строит env-префикс, читает sentinel
отчёта, шлёт Telegram-алерт. Образец `serial_gate`/`cancel`/`self_deploy`.
- **Сходимость** базы после failed/cancelled (FR-2) — этим же deploy-time self-heal; `cancel_task`
(ORCH-090) **не расширяется**, фоновый janitor **не вводится**.
- **Наблюдаемость** — хук пишет sentinel `hygiene`, Phase-C finalizer читает и шлёт Telegram-алерт
(best-effort, never-raise).
- **Инвариант** «main checkout — deploy/worktree-management база, НЕ workspace» документируется
(INFRA.md + architecture/README.md); de-facto энфорс — сам resilient-pull.
## Кросс-каттинг-инварианты (обязательны к соблюдению будущими задачами)
- **INV-HYGIENE-1 (никогда `-x`):** hygiene-`git clean` — только `git clean -fd`. `-x` удалил бы
gitignored `.env` (прод-секреты) / `data/*.db` (БД прода) / `build/`. Анти-регресс — статический тест.
- **INV-HYGIENE-2 (явные excludes):** `.deploy-prev-image-*` (rollback, `deploy_prod_prev_image_file`)
и `deploy-hook.log` — untracked-но-НЕ-ignored → обязательны `-e`-исключения; их удаление сломало бы
rollback.
- **INV-HYGIENE-3 (скоуп = `$REPO`):** гигиена оперирует только рабочим деревом deploy-базы;
sibling `<repos_dir>/.deploy-state-*` / `.merge-lease-*.json` и `.git/worktrees/*` — вне области.
- **Self-hosting safety (NFR-1):** никогда не трогать `main` на remote, не force-push, не рестартить
прод вне штатного гейта, не сносить worktree/ветки других активных задач.
- **Нулевая регрессия (NFR-5):** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` /
machine-verdict ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — байт-в-байт. Это
устойчивость deploy-пути, **не** Quality Gate и **не** стадия.
## Связи
- Дополняет: adr-0007 (executable self-deploy, ORCH-036), adr-0008 (image-freshness, ORCH-058).
- Не нарушает: adr-0026 (STOP/cancel, ORCH-090) — каскад cancel не трогается.
## Откат
`ORCH_CHECKOUT_HYGIENE_ENABLED=false` → прод-деплой байт-в-байт до ORCH-112 (голый `git pull origin main`).

View File

@@ -0,0 +1,94 @@
---
work_item: ORCH-114
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0045: Durable transition-ownership lease + expected-stage CAS — единое владение side-effectful переходами стадий
- **Статус:** proposed
- **Дата:** 2026-06-15
- **Задача:** ORCH-114 (bug → escalate full-cycle; системный наследник кластера ORCH-110/111/112/113)
- **Детальный ADR:** `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`
- **Обобщает:** `adr-0043` (ORCH-113 in-memory finalizer-liveness — отправная точка)
- **Уточняет/опирается:** `adr-0011` (reaper/lease-reclaim ORCH-065), `adr-0040` (бюджеты ORCH-109),
`adr-0042` (merge-retest ORCH-110), `adr-0027` (merge-lease ORCH-043), `adr-0029` (coverage-ratchet ORCH-027),
ORCH-071/073/093 (SHA-in-main / already-in-main), ORCH-036 (`INITIATED` self-deploy)
## Контекст
Корневой класс инцидент-цепочки ORCH-110/111/112/113: **у side-effectful переходов стадий нет единого
владения**. `db.update_task_stage` — голый `UPDATE … WHERE id=?` без CAS (`db.py:671679`); `advance_stage`
ре-ентерабельна без защиты и исполняет минуты-длинные необратимые под-гейты (`deploy-staging → deploy`:
security→merge-retest→coverage→image-freshness; `deploy → done`: `merge_pr`/ratchet/proof-of-merge) **до**
единственной записи стадии. ≥5 акторов входят в переход независимо (монитор/webhook/reconciler F-1/reaper/
Phase-C finalizer) + 6 путей пишут стадию в обход `advance_stage` (5× `gitea.py`, 1× `plane.py:806`).
ORCH-113 (`finalizer_liveness`) закрыл это лишь in-memory, reaper-Tier-2, `deploy-staging`, теряя владение
на рестарте — остаточный кросс-путь дал двойной эффект и противоречие rollback↔done (ORCH-111, job 1914/PR #130).
## Решение
Два комплементарных аддитивных слоя под единым kill-switch, never-raise:
1. **Durable transition-lease** — новая аддитивная таблица `transition_lease`
(`task_id PK, owner, owner_pid, owner_boot_id, run_id, stage, acquired_at`; `CREATE TABLE IF NOT EXISTS`,
паттерн `repo_freeze`/`coverage_baseline`). Владение захватывается на **входе** в side-effectful регион
`advance_stage` (рёбра `deploy-staging→deploy`, `deploy→done`, Phase C `run_deploy_finalizer`); второй
актор, увидев **живого** владельца, не стартует под-гейты вовсе (предотвращение класса, а не починка).
Release — в `try/finally`. **Liveness = `owner_pid` + `owner_boot_id`**, НЕ heartbeat (heartbeat отвергнут
тем же доводом, что в adr-0043: блокирующий 900s re-test не может его бить). Реклейм мёртвого/устаревшего
(pid мёртв ИЛИ boot-id чужой) — немедленно; зависший живой добивается Tier-3.
2. **Expected-stage CAS**`update_task_stage_cas(task_id, expected_stage, new_stage)`
(`UPDATE tasks SET stage=? … WHERE id=? AND stage=?`, rowcount==1 ⇒ выиграл; 0 ⇒ проиграл → аборт без
побочных эффектов). Покрывает остаточное окно гонки И 6 обходных путей. Без epoch-колонки: для текущей
модели стадия *и есть* версия (epoch — задокументированное форвард-расширение под `--workers>1`).
**Осведомлённость акторов:** reaper консультирует durable-lease на **всех** путях (обобщение ORCH-113):
живой → defer, мёртвый → реклейм, Tier-3 маркер игнорирует; reconciler F-1 и webhook (Approved/Confirm
Deploy) — новый skip-guard по образцу escalated/Blocked/task-deps. `finalizer_liveness` сохранён без правок
как поведение при **выключенном** ORCH-114 (надстройка durable-слоя поверх).
**Умное восстановление (FR-4)**НЕ новый recovery-мозг, а композиция: `requeue_running_jobs` (есть) +
startup stale-clear (boot-id mismatch ⇒ старые lease мертвы) + идемпотентность re-drive через
**авторитетные durable-факты предшественников** (SHA-in-main ORCH-071/073, `INITIATED` ORCH-036,
coverage-ratchet CAS ORCH-027). Lease лишь гарантирует **последовательную**, не конкурентную, их проверку.
**Бюджет (NFR-6):** lease без собственного TTL; жёсткий потолок возраста = Tier-3 `reaper_max_running_s`
(5400), reaper при реапе force-освобождает lease. Сквозной инвариант `5400 > Σ(≈4460)+grace` и
`reaper_finalize_grace_s`/`reaper_max_running_s`**не тронуты**.
**Конфиг:** `transition_lease_enabled=True` (kill-switch) + `transition_lease_repos=""` (CSV; пусто →
self-hosting only, паттерн coverage/serial-gate). Leaf `src/transition_lease.py` never-raise.
**Инварианты:** `STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict-ключи / схемы
**существующих** таблиц — байт-в-байт; +1 аддитивная таблица; механизм не рестартит прод, не пушит/
force-push `main`, не трогает detached-деплой (NFR-5). Hot-path `claim_next_job` не тронут (fail-open).
## Альтернативы
- Только CAS (без lease) — не предотвращает двойной side-effect в полёте.
- Только lease (без CAS) — не покрывает 6 обходных путей + окно consult→acquire.
- Heartbeat-liveness — блокирующий re-test не бьёт heartbeat (довод adr-0043).
- Lease-файл per-task — CAS на стадию всё равно DB-операция; БД когерентнее, merge-lease-файл per-repo для
иной задачи (сериализация мержей), не дублируется.
- epoch-колонка / sub-state `finalizing` в `jobs.status` / per-stage grace на Σ — отвергнуто (как в adr-0043:
меняет семантику/нарушает бюджет/неиспользуемо).
## Последствия
- (+) Класс двойного эффекта закрыт в корне; конкурентный/после-рестартовый/reconciler/webhook пути покрыты.
- (+) Рестарт-safe без нового таймера; boot-id готовит multi-process; бюджет и инварианты конвейера целы; +1 таблица.
- (+) Дыра обходных путей gitea/plane закрыта CAS; откат — один env-флаг.
- () Полная multi-writer эксклюзия валидна при одном процессе/одной БД (как adr-0043); durable делает её
корректной для рестарта, но `--workers>1`-верификация — вне объёма (риск в `10-tech-risks.md`).
## Связи
- Обобщает `adr-0043`; опирается на `adr-0011`/`adr-0040`/`adr-0042`/`adr-0027`/`adr-0029` и ORCH-071/073/093/036.
- Маркеры (ORCH-078/TRACEABILITY): блоки reaper/finalizer-liveness/stage-engine несут ORCH-065/109/110/113 +
новый `ORCH-114`; правки сверяются с их ADR (анти-археология — этот сводный сквозной ADR).
- Детально: `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`.
</content>

View File

@@ -0,0 +1,121 @@
---
work_item: ORCH-117
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0046: Sandbox-only fail-closed гард записи в Plane из тест-процесса
Сквозной (cross-cutting) ADR. Вводит инвариант **«мутирующая запись в Plane из тест/worktree-процесса
физически невозможна в боевой проект; sandbox — только под явным opt-in»** поверх **общего**
Plane-клиента `src/plane_sync.py` (три примитива записи, используемые ВСЕМИ проектами общего
инстанса) и нового тест-харнесс-инварианта `tests/conftest.py`. Детальное решение задачи —
`docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`.
> Регистрируется как сквозной, т.к. правит **системно используемые** примитивы записи
> `update_issue_state`/`add_comment`/`_set_issue_state_direct` и вводит новый рантайм-компонент
> (leaf `src/plane_write_guard.py`), затрагивающий индикацию (слой B, ORCH-066) всех проектов.
> Кросс-каттинг с adr-0028 (deploy-status guard, ORCH-094) и adr-0009 (staging-tolerance, ORCH-061):
> оба — потребители того же `plane_sync`; гард для них — no-op в боевом/staging рантайме.
## Статус
Proposed
## Контекст
Инцидент **ORCH-114**: тестовый/worktree-процесс (`python -m pytest` из worktree) выполнил
**реальную** запись в Plane против **боевого** проекта ORCH (`PATCH state=<Done>` + комментарий) —
«ложный Done» на боевой доске. Корень (сверено по коду `src/plane_sync.py`):
1. `PLANE_HEADERS`/`PROJECT_ID` (боевой токен + боевой дефолтный проект) **захвачены на импорте**
модуля (стр. 17/57) → подмена env/токена постфактум бесполезна.
2. Тестовые `os.environ.setdefault("ORCH_PLANE_API_TOKEN",…)`**no-op** в контейнере с уже
установленной боевой переменной.
3. Все мутации сходятся в **три** примитива (`update_issue_state`/`add_comment`/
`_set_issue_state_direct`), и ни один **не** проверяет тест-контекст и легитимность целевого
проекта.
Симметричная защита для Telegram (`tests/conftest.py::_no_telegram`) существует и работает по тому же
классу проблем («pytest на проде слал реальные сообщения»); для Plane-записи её **не было**.
## Решение
**Fail-closed гард на низком чокпоинте**, в момент вызова, двумя независимыми sandbox-bound слоями.
### D1 — Рантайм-leaf `src/plane_write_guard.py` (never-raise)
Чистый leaf (паттерн `serial_gate`/`cancel`/`deploy_status_guard`): импортирует только `config`,
лениво `db`. `decide(project_id, op, work_item_id) -> (ok: bool, reason: str)`:
1. `not _in_test_process()`**ALLOW** (боевой/staging рантайм — no-op, byte-for-byte).
2. `project_id` нерезолвим → **BLOCK** `ambiguous-target` (fail-closed, NFR-1).
3. `not plane_test_write_enabled`**BLOCK** `opt-in-disabled`.
4. `project_id ∉ sandbox-allowlist`**BLOCK** `prod-project-in-test` (sandbox-only даже при opt-in).
5. иначе → **ALLOW** `sandbox-opt-in` (audit INFO).
Врезается в 3 примитива `plane_sync` сразу после `_resolve_project_id` и **до** любого сетевого шага;
на BLOCK — структурный аудит + `return` (ни GET, ни PATCH/POST).
### D2 — Детект `_in_test_process()`
`"pytest" in sys.modules or PYTEST_CURRENT_TEST` (call-time). Боевой/staging рантайм
(`uvicorn src.main:app`) pytest в свой процесс не импортирует → детект там никогда не срабатывает
(нулевая регрессия). worktree-`python -m pytest` (инцидентный путь) детектируется гарантированно.
### D3 — Conftest-floor `tests/conftest.py::_plane_sandbox_only`
Autouse-фикстура (паттерн `_no_telegram`/`_reset_webhook_secrets`/`_disable_*`) форсит во ВСЕХ тестах
безопасные дефолты (`plane_test_write_enabled=False`, allowlist = канонический SANDBOX id),
перекрывая любую боевую переменную из окружения. Sandbox-e2e ре-энейблит opt-in **после** autouse
(scoping реальной записи на себя). Слой независим от рантайм-leaf → двойной default-deny.
### D4 — Реверс через opt-in, БЕЗ kill-switch (норматив)
Единственный реверсивный регулятор — sandbox-bound opt-in `plane_test_write_enabled` (+ allowlist
`plane_test_sandbox_projects`). **Намеренно нет** prod-блок kill-switch: выключатель, обнуляющий
prod-блок в тест-процессе, был бы «чёрным ходом» (NFR-6). Прецедент — `_no_telegram` (тоже без
«разрешить»-флага). **Анти-дрейф (норматив на будущее):** не вводить общий kill-switch гарда,
переоткрывающий прод-запись из pytest.
### D5 — Скоуп: НЕ `*_repos`
В отличие от гейт-leaf'ов (`serial_gate`/`coverage_gate`, scope по репо, т.к. *действуют* на репо),
гард защищает запись в **любой** боевой проект общего workspace (включая боевой enduro) → скоупа по
репо нет; гейты — `_in_test_process()` + opt-in (как у observer-leaf `lessons`).
## Инварианты (что НЕ меняется)
`STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи
(`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/`coverage_status:`) /
схема БД — **байт-в-байт не тронуты**. Это bugfix-изоляция клиента Plane, **не** Quality Gate и
**не** стадия. Боевой и staging рантаймы — byte-for-byte (no-op гарда). adr-0028 (deploy-status
guard) / adr-0009 (staging-tolerance) / ORCH-066 (статусная модель) в проде/стейджинге не затронуты.
## Конфиг
| Ключ | Env | Дефолт |
|------|-----|--------|
| `plane_test_write_enabled` | `ORCH_PLANE_TEST_WRITE_ENABLED` | `False` |
| `plane_test_sandbox_projects` | `ORCH_PLANE_TEST_SANDBOX_PROJECTS` | `8c5a3025-4f9d-4190-b79f-fa06276bb27e` |
## Последствия
- **+** Прод-запись в Plane из pytest/worktree физически невозможна независимо от токена; ORCH-114
закрыт у источника и стал видимым (аудит).
- **+** Нулевая регрессия боевого/staging рантайма и гейтов/схемы БД.
- **** Детект завязан на «pytest-в-процессе» (теоретический ложноположительный риск — TR-1) и
умышленный отказ от kill-switch требует явной фиксации (TR-4). См. `10-tech-risks.md`.
- **Откат:** снять врезку гарда + autouse-фикстуру + 2 конфиг-ключа → поведение до ORCH-117 (дефект
возвращается).
## Ссылки
- Детально: `docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`
- Риски: `docs/work-items/ORCH-117/10-tech-risks.md`
- Связанные: [adr-0028](adr-0028-terminal-window-aware-deploy-status-guard.md) (ORCH-094),
[adr-0009](adr-0009-staging-infra-tolerance.md) (ORCH-061),
[adr-0034](adr-0034-lessons-journal.md) (observer-leaf без `*_repos`)
- Сверено по коду: `src/plane_sync.py:17,57,846-889,1038-1051`, `tests/conftest.py`,
`scripts/staging_check.py:283`

View File

@@ -0,0 +1,114 @@
---
work_item: ORCH-118
stage: architecture
author_agent: architect
status: accepted
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# adr-0047: Нормативная политика использования LLM + карта call-site'ов (control-path-ось «avoidable»)
> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-118, влияющее на **весь** оркестратор:
> нормативная политика использования LLM, три ортогональных оси, определение «avoidable LLM control
> path» и снимок-карта LLM-консультаций, прибитая к коду структурными тестами. Локальная детализация —
> `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`.
## Статус
Accepted
## Контекст
RCA-цепочка ORCH-114/117 (и 110/111/112/113) показала корневой класс: у side-effectful и решающих
control-path'ов не было единого детерминированного владения; местами решение брал LLM-агент «потому
что удобно», хотя по сути это исполнение фиксированных команд + маппинг результата — лишний
недетерминизм, задержка и расход токенов в точке ветвления.
Оркестратор не имел **нормативного критерия** «где LLM нужен, а где это avoidable control path» и
**карты** мест вызова LLM, прибитой к коду. Без них любая будущая правка control-path'а могла снова
ввести LLM «на удобстве», а «вслепую» убирать LLM нельзя — часть путей несёт настоящее суждение
(анализ, архитектура, написание кода, ревью).
**Ground-truth кода (ORCH-118, сверено):** единственный транспорт LLM-консультации в `src/**`
`launcher._spawn` (`launcher.py:472`, CLI `610-614`); иного LLM-транспорта нет (нет SDK-импортов /
прямого HTTP Anthropic / второго сборщика). 6 ролей-агентов консультируют через него; D1/D2
(`deploy-finalizer`/`post-deploy-monitor`) перехватываются в `launch_job` **до** `_spawn`
(`launcher.py:389/394`) — слот есть, консультации нет. Потребитель вывода каждой роли — конкретный
`check_*`/`_parse_*` в `src/qg/checks.py`.
## Решение
### D1 — Три ортогональных оси (нормативно для всего оркестратора)
1. **consultation ≠ transport/slot** — «потребляет суждение LLM» ≠ «спавнит процесс / занимает слот
агента» (capability ≠ consultation).
2. **control-path (C) ≠ artifact-producer (P)** — определяется кодом-потребителем: C — `check_*`
ветвится на machine-verdict, написанном LLM; P — детерминированный гейт судит артефакт независимо
(файлы/CI).
3. **деривируемость вердикта** — вердикт C-консультации либо детерминированная функция tool-сигналов
(exit-code `pytest`/smoke/`staging_check.py`/деплоя), либо настоящее суждение.
### D2 — Нормативное определение «avoidable LLM control path»
> Call-site — **avoidable LLM control path** ⟺ **(i)** C-консультация (LLM-вердикт потребляется
> потоком управления) **И (ii)** вердикт деривируем из tool-сигналов, которые оркестратор уже
> вычисляет → LLM не добавляет информации.
Целевой набор (доказательно из `src/qg/checks.py`): **avoidable = {tester, deployer}**;
control-path-но-keep = `{reviewer}`; не-control-path (P, keep) = `{analyst, architect, developer}`;
уже детерминированы (вне консультаций) = `{deploy-finalizer, post-deploy-monitor}`.
### D3 — Нормативная политика использования LLM (`docs/architecture/llm-usage-policy.md`)
Принцип: **«LLM — только там, где требуется настоящее суждение».** Критерий keep vs replace —
через оси D1 (является ли путь control path; деривируем ли вердикт; обратимость; влияние на
автономность NFR-2). **Требование:** любая новая/изменённая control-path-консультация обязана
обосновать использование LLM против этой политики; reviewer контролирует это как обзорную ось
(в духе ORCH-079) — **как требование, не как новый машинный гейт**.
### D4 — Карта как снимок, прибитый к коду
`docs/architecture/llm-call-sites.md` — инвентарь + control-path-разметка + классификация со
схемой полей и машинным блоком (детали — work-item ADR-001 D2/D4). Структурные тесты
`tests/test_llm_call_site_inventory.py` (offline) держат инварианты: транспорт-агностичный
двусторонний инвариант единственной точки, отсутствие консультации в детерминированных путях,
control-path-разметка сверена с `src/qg/checks.py`, avoidable-набор = `{tester, deployer}`.
### D5 — Roadmap детерминизации (`docs/architecture/llm-determinization-roadmap.md`)
Рекомендованный первый срез — **deployer (staging-status)** (`replace-deterministic-now`: чистый
маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C ORCH-036; опора на
прецедент D1/D2). Затем — **tester-гибрид** (`needs-hybrid-fallback`). Кандидаты — **по роли**,
без конкретных Plane-ID (NFR-6).
### D6 — Скоуп и инварианты (нормативно)
ORCH-118 — **docs + tests only**: `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен не реализуются;
follow-up Plane-ID не фиксируются. Self-hosting-безопасно (только чтение кода + запись docs/tests).
**Норматив сопровождения (durable):** менял места вызова LLM **или** потребителя вердикта в
`src/qg/checks.py` → обнови карту/разметку и политику в **том же PR** (иначе тесты D4 красные).
## Альтернативы
- **Машинный гейт-enforcement политики (новый QG)** — отвергнуто: политика нормативно-описательная,
как ось трассировки ORCH-078; новый QG увеличил бы поверхность риска без необходимости (FR-6 §QG).
- **Реализация раннеров в этой же задаче** — отвергнуто: inventory-first по требованию заказчика;
«вслепую» убирать LLM рискованно без утверждённой карты.
- **Привязка к конкретным follow-up ID** — отвергнуто (NFR-6, корень отклонённой R2).
## Последствия
- **+** Единый нормативный критерий и код-привязанная карта закрывают класс «LLM на удобстве» и
делают замены предсказуемыми; автономность защищена политикой.
- **** Карта — снимок: эволюция `src/qg/checks.py` требует со-обновления карты (держится тестами).
*Митигейшн:* запланированный норматив сопровождения, тест указывает точку дрейфа.
- **Откат:** удаление/правка `docs/architecture/llm-*.md` + тест-файла + секции README; рантайм не
затронут.
## Ссылки
- Work-item ADR: `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`
- BRD/TRZ/AC: `docs/work-items/ORCH-118/{01-brd,02-trz,03-acceptance-criteria}.md`
- Сверено по коду: `src/agents/launcher.py`, `src/qg/checks.py`, `.openclaw/agents/*.md`
- Связанные: ORCH-036 (детерминированный self-deploy), ORCH-061 (`staging_verdict`),
ORCH-077/079 (docs/prompts-only прецедент + reviewer-ось обзорных доков), ORCH-114/117 (RCA-трек)
</content>

View File

@@ -0,0 +1,92 @@
---
work_item: ORCH-115
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---
# adr-0048: Детерминированный staging-раннер — первый реализованный срез determinization-roadmap
> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-115, влияющее на **весь**
> оркестратор: вводит новый компонент-leaf `src/staging_runner.py`, снимает первую
> avoidable LLM-консультацию (`deployer`/`staging-status`, A6) и переводит rank-1
> determinization-roadmap из «план» в «реализовано». Локальная детализация (все решения
> D1D11) — `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`.
## Статус
Proposed
## Контекст
ORCH-118 ([adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)) зафиксировал
нормативную политику и карту LLM-консультаций и назвал **avoidable LLM control paths =
`{tester, deployer}`**, поставив **deployer (staging-status)** первым срезом
(`first_slice = yes`, `replace-deterministic-now`, `hybrid_needed = no`). ORCH-118 раннеры
**не реализовывал** (docs+tests). ORCH-115 — первая фактическая реализация этого среза.
Вердикт `staging_status:` на стадии `deploy-staging` сейчас эмитит LLM-агент `deployer`, но
он есть **чистый маппинг exit-кода** `scripts/staging_check.py` (infra-tolerance ORCH-061
уже внутри скрипта), а гейт `check_staging_status` детерминирован. Это удовлетворяет обоим
условиям «avoidable»: C-консультация **и** деривируемый вердикт. Прецедент детерминированной
замены агента (`launch_job`-перехват до `_spawn`, D1/D2 `deploy-finalizer`/`post-deploy-monitor`)
и эталон «детерминированный джоб → `advance_stage`» (`run_deploy_finalizer`) уже работают в
проде — архитектурный риск снят.
## Решение
**Новый leaf `src/staging_runner.py` + перехват в `launch_job` до `_spawn`** (рядом с D1/D2).
На `deploy-staging` для in-scope репо джоб `deployer` обрабатывает раннер: исполняет
staging-сюиту через `proc_group` (tree-kill, ORCH-110), маппит exit-код единым контрактом
`self_deploy.map_exit_code_to_status`, пишет `15-staging-log.md` (тот же machine-key
`staging_status:`), вызывает **существующий** `advance_stage(finished_agent="deployer")`.
Кросс-каттинговые инварианты (сохранены **байт-в-байт**):
- `STAGE_TRANSITIONS` (`src/stages.py`), реестр и имена `QG_CHECKS`/`check_*`/`_parse_*`,
machine-verdict-ключи (`staging_status:`/`deploy_status:`/`verdict:`/`result:`/
`security_status:`/`coverage_status:`), **схема БД** — не тронуты. Это замена *продюсера*
артефакта, не гейта/стадии.
- Единственный транспорт LLM-консультации (`launcher._spawn`/S0,
[llm-usage-policy.md](../llm-usage-policy.md) §5) — соблюдён: раннер **не зовёт LLM**;
второй транспорт не вводится.
- Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре
`deploy-staging`) + grace) — соблюдён **без** правки `reaper_max_running_s` (раннер-таймаут
600s ≤ прежнего LLM-окна).
- Граница ORCH-112/ORCH-114: transition-lease берётся **внутри** `advance_stage`; раннер
lease/гигиену не модифицирует.
Скоуп — **self-hosting only** (`staging_runner_repos=""``is_self_hosting_repo`), под
kill-switch `staging_runner_enabled` (off → `_spawn` LLM-deployer'а байт-в-байт). never-raise
во всех публичных функциях; **двухуровневый исход** (verdict при исполнившейся сюите; bounded
defer → fail-closed на tool-error/таймауте) убирает с staging-ребра RCA-класс ORCH-110 (инфра
≠ код-фейл).
**Эволюция карты LLM (норматив сопровождения, в том же PR — D11 локального ADR):**
`llm-call-sites.md` (A6 → реализовано детерминированно), `llm-determinization-roadmap.md`
(rank 1 deployer → реализован; инвариант «ровно один `first_slice`» цел), `llm-usage-policy.md`
(§5 — транспорт не нарушен), плюс анти-дрейф-тесты (`test_llm_call_site_inventory.py`/
`test_llm_determinization_docs.py`). Эти правки коуплены к коду → применяются в development
атомарно с реализацией, не в architecture-стадии.
## Последствия
- **+** Минус один avoidable LLM control path; первый доказанный раннер-паттерн замены
C-консультации (опора для второго кандидата — `tester`-гибрид, rank 2).
- **+** Дешевле/быстрее/детерминированнее собственный `deploy-staging`; нет токенов/латентности
LLM в точке ветвления.
- **+** Паттерн переиспользуем: leaf + перехват до `_spawn` + `advance_stage` — шаблон для
будущих срезов и для Phase 2 (project deploy contract не-self репо).
- **** Новый компонент + врезка + defer-механика. Митигейшн: never-raise leaf, kill-switch
(fail-safe к LLM), без схемы БД, структурное покрытие.
- **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` → прежний LLM-путь на `deploy-staging`
байт-в-байт.
## Ссылки
- Локальный ADR: `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`
- Политика/карта/roadmap: [llm-usage-policy.md](../llm-usage-policy.md),
[llm-call-sites.md](../llm-call-sites.md), [llm-determinization-roadmap.md](../llm-determinization-roadmap.md),
[adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)
- Прецеденты: D1/D2 (`launcher.py:389/394`), `run_deploy_finalizer` (`stage_engine.py:2010`),
`proc_group` (ORCH-110, [adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md)),
transition-lease (ORCH-114, [adr-0045](adr-0045-transition-ownership-lease-and-stage-cas.md))

View File

@@ -0,0 +1,105 @@
---
work_item: ORCH-123
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---
# adr-0049: Граница исполнения docker — все docker-операции host-side, не изнутри app-контейнера
> **Сквозной (cross-cutting) ADR.** Кодифицирует инвариант **«docker-операции оркестратора
> исполняются host-side через доверенный ssh-канал, никогда изнутри прод-контейнера»**, охватывающий
> компоненты ORCH-036/058/115/123/101, и **амендит** execution-strategy-решение
> [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5). Поводом стала задача ORCH-123 (баг:
> staging-runner отклонился от инварианта). Локальная детализация (D1D9) —
> `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`.
## Статус
Proposed
## Контекст
Прод-контейнер `orchestrator` (8500) **не содержит docker CLI** (`Dockerfile:11`:
`openssh-client git curl ca-certificates` + pinned gitleaks; `python:3.12-slim` docker не несёт).
`/var/run/docker.sock` смонтирован rw + `group_add 999` (ORCH-040 «МИНА 1»), но **клиента, который
бы им воспользовался, нет** — сознательно: добавление CLI/SDK активировало бы root-эквивалентный путь
исполнения для всего, что бежит в контейнере (вкл. LLM-агентов). Поэтому в оркестраторе сложился
**инвариант исполнения**, ранее не выделенный в отдельный ADR:
- **ORCH-036** (`self_deploy.build_deploy_command`, [adr-0007](adr-0007-executable-self-deploy.md)) —
прод-деплой исполняется host-side через `ssh + setsid bash <hook> --deploy` на `127.0.0.1`.
- **ORCH-058** (`image_freshness`, [adr-0008](adr-0008-staging-image-provenance.md)) — ребилд
staging-образа (`ssh … bash <hook> --build-staging`) и инспекция revision
(`image_revision(ssh_target=…)`) — host-side; модуль прямо документирует:
*«docker lives on the HOST (the container ships only openssh-client git)»*.
- **ORCH-101** ([adr-0036](adr-0036-replication-foundation-host-parametrization.md)) — host-параметры
канала (`deploy_ssh_*`, `deploy_host_repo_path`, `repos_dir`/`host_repos_dir`) расхардкожены.
**ORCH-115** ([adr-0048](adr-0048-deterministic-staging-runner.md)), заменяя LLM-деплойера
детерминированным `staging_runner`, **отклонился** от инварианта: зашил `docker exec` **изнутри**
прод-контейнера через `proc_group → Popen``FileNotFoundError: docker` → постоянный
environment-дефект, ложно маршрутизированный как транзиентная инфра → DEFER → fail-closed FAILED →
**откат `deploy-staging → development`** (винит код задачи за дефект окружения раннера). Инцидент
ORCH-116/ORCH-123.
## Решение
**Кодифицировать инвариант (нормативно):** docker-операции оркестратора (`docker`/`docker compose`/
`docker exec`/`docker inspect`/`docker tag`) исполняются **host-side** через доверенный ssh-канал
(`deploy_ssh_host=127.0.0.1`, ключ смонтирован, `openssh-client` в образе) — **никогда** изнутри
прод-контейнера, который docker CLI не несёт. `/var/run/docker.sock` **не используется** изнутри
контейнера; docker CLI/SDK в образ **не добавляется** (любое исключение — отдельный явный
security-review: socket-из-контейнера = root-эквивалент на хосте, обслуживающем все проекты).
**ORCH-123 приводит `staging_runner` в соответствие** (амендит adr-0048 D3/D5):
- **D3 (амендмент adr-0048):** `staging_runner.build_staging_command` теперь обёртывает
`docker exec orchestrator-staging python3 staging_check.py …` в `ssh <user>@<host> '<…>'` (зеркало
`image_freshness.image_revision(ssh_target=…)`). Внутренняя команда сюиты и exit-код-контракт — те
же; меняется лишь **инициатор/канал**.
- **D5 (амендмент adr-0048 двухуровневого исхода):** введён **третий** класс исхода `permanent-env`
(зеркало `merge_gate.classify_retest_failure`, ORCH-110); корневой инвариант — **«сюита не
исполнилась» (environment ИЛИ транзиентная инфра) НИКОГДА не оканчивается код-фейл-откатом и не жжёт
developer-retry**; откат — только для реально исполнившейся сюиты с `exit≠0`. Терминал исчерпания
DEFER изменён с fail-closed-FAILED+advance на **infra-HOLD + alert** (как ORCH-110 D3).
Кросс-каттинговые инварианты (сохранены **байт-в-байт**, как adr-0048):
- `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_staging_status`/`_parse_staging_status` /
machine-verdict-ключи (`staging_status:`/`deploy_status:`/…) / **схема БД** — не тронуты (замена
*стратегии исполнения продюсера*, не гейта/стадии).
- Единственный транспорт LLM-консультации (`launcher._spawn`/S0, [adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md))
— соблюдён (раннер LLM не зовёт).
- Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре) + grace) — не
растёт (host-side ssh заменяет in-container call, окно ≤ `staging_runner_timeout_s`).
- Граница transition-lease ORCH-114 — берётся внутри `advance_stage`; раннер не трогает.
Скоуп — **self-hosting only** (`staging_runner_repos=""``is_self_hosting_repo`); под флагами
`staging_runner_enabled` (→ LLM-путь) и **новым** `staging_runner_exec_host_side` (дефолт `True`
фикс; `False` → прежний in-container call). never-raise во всех публичных функциях.
## Последствия
- **+** Инвариант «docker host-side» выделен и задокументирован → будущие компоненты не повторят
отклонение ORCH-115; reviewer ловит in-container docker как регресс инварианта.
- **+** staging-сюита реально исполняется в проде; инфра/environment ≠ код-фейл на staging-ребре
(закрыт RCA-класс ORCH-110 на этом ребре полностью); анти-over-tolerance цел.
- **+** Без расширения привилегий (нет docker CLI/SDK в контейнере, сокет не используется); согласовано
с ORCH-036/058.
- **** Remote tree-kill ограничен локальным ssh-клиентом (как `image_freshness.rebuild_staging_image`);
backstop — bounded таймаут внутри `staging_check.py`.
- **** Permanent-env/исчерпавшая-DEFER задача держится на `deploy-staging` (блокирует serial-gate репо
до починки оператором) — принятый tradeoff (зеркало ORCH-110), self-hosting only.
- **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` (→ LLM) или `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false`
(→ in-container call).
## Ссылки
- Локальный ADR: `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`
- Амендит: [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5 ORCH-115)
- Опирается на: [adr-0007](adr-0007-executable-self-deploy.md) (ORCH-036 self-deploy ssh),
[adr-0008](adr-0008-staging-image-provenance.md) (ORCH-058 image-freshness host-side docker),
[adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md) (ORCH-110 proc_group +
classify + infra-tolerance), [adr-0036](adr-0036-replication-foundation-host-parametrization.md)
(ORCH-101 host-параметризация)
- Сверено по коду: `src/staging_runner.py`, `src/self_deploy.py:220`, `src/image_freshness.py:185/246`,
`scripts/orchestrator-deploy-hook.sh:166/197`, `Dockerfile:11`, `docker-compose.yml`

View File

@@ -0,0 +1,115 @@
---
work_item: ORCH-116
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---
# adr-0050: Детерминированный test-раннер — второй реализованный срез determinization-roadmap (tester-гибрид)
> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-116, влияющее на **весь**
> оркестратор: вводит новый компонент-leaf `src/test_runner.py`, снимает вторую avoidable
> LLM-консультацию из потока управления (`tester`/`result:`, A5) и переводит rank-2
> determinization-roadmap из «план» в «реализовано». Локальная детализация (все решения
> D1D12, включая tester-специфичную анти-коллизию `status:` D6.1) —
> `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`.
## Статус
Proposed
## Контекст
ORCH-118 ([adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)) зафиксировал нормативную
политику и карту LLM-консультаций и назвал **avoidable LLM control paths = `{tester, deployer}`**.
Первый срез — **deployer (staging-status, rank 1)** — реализован **ORCH-115**
([adr-0048](adr-0048-deterministic-staging-runner.md)). Второй кандидат — **tester (rank 2,
`needs-hybrid-fallback`, `hybrid_needed = yes`, `first_slice = no`)**. ORCH-116 — его фактическая
реализация.
Вердикт `result:` на стадии `testing` сейчас эмитит LLM-агент `tester`, но **PASS/FAIL-ядро** есть
**чистый маппинг** exit-кода `pytest` + read-only smoke, а гейт `check_tests_passed`
(`_parse_tests_verdict`) детерминирован и читает **только** frontmatter `result:` (+ legacy
`verdict:`/`status:`). Это удовлетворяет обоим условиям «avoidable»: C-консультация **и**
деривируемый вердикт. **Гибрид-нюанс:** прежний промпт нёс ещё и настоящее суждение (триаж падений,
маппинг TC↔критерии) — поэтому ORCH-116 выносит из потока управления **только PASS/FAIL-исполнителя**,
оставляя LLM допустимым лишь как будущий **off-control-path** триаж (Phase 2, не control-path).
Прецедент детерминированной замены агента (`launch_job`-перехват до `_spawn`, D1/D2 +
**рабочий эталон `src/staging_runner.py`** ORCH-115) и эталон «детерминированный джоб → `advance_stage`»
уже в проде — архитектурный риск замены снят.
## Решение
**Новый leaf `src/test_runner.py` + перехват в `launch_job` до `_spawn`** (рядом с D1/D2/ORCH-115).
На `testing` для in-scope репо с резолвимым тест-контрактом джоб `tester` обрабатывает раннер:
исполняет регресс `pytest <target>` **в worktree ветки** через `proc_group` (tree-kill, ORCH-110) +
опциональный read-only smoke, маппит exit-код единым контрактом `self_deploy.map_exit_code_to_status`
(транслируя токены в `PASS`/`FAIL`), пишет `13-test-report.md` (тот же machine-key `result:`),
best-effort пушит лог в фичеветку, вызывает **существующий** `advance_stage(current_stage="testing",
finished_agent="tester")`.
Кросс-каттинговые инварианты (сохранены **байт-в-байт**):
- `STAGE_TRANSITIONS` (`src/stages.py`), реестр и имена `QG_CHECKS`/`check_tests_passed`/
`_parse_tests_verdict`/прочих `check_*`/`_parse_*`, machine-verdict-ключи (`result:`/`verdict:`/
`status:`/`staging_status:`/`deploy_status:`/`security_status:`/`coverage_status:`), **схема БД**
не тронуты. Это замена *продюсера* артефакта, не гейта/стадии.
- Единственный транспорт LLM-консультации (`launcher._spawn`/S0,
[llm-usage-policy.md](../llm-usage-policy.md) §5) — соблюдён: раннер **не зовёт LLM**; второй
транспорт не вводится; будущий off-control-path триаж — вне control-path (не контр-пример политике).
- Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` (5400) > Σ(работ на ребре)) —
соблюдён **без** правки `reaper_max_running_s`: ребро `testing` отдельно от `deploy-staging`, окно
раннера ≤900s ≤ прежнего LLM-окна `agent_timeout_seconds` (1800s).
- Граница ORCH-112/ORCH-114/ORCH-115: transition-lease берётся **внутри** `advance_stage`; раннер
lease/гигиену/`staging_runner` не модифицирует.
Скоуп — **self-hosting only** (`test_runner_repos=""``is_self_hosting_repo` + резолв
тест-контракта `_has_test_contract`, в Phase 1 = self-hosting), под kill-switch
`test_runner_enabled` (off → `_spawn` LLM-tester'а байт-в-байт). never-raise во всех публичных
функциях; **двухуровневый исход** (verdict при исполнившейся сюите; bounded defer → fail-closed на
tool-error/таймауте) убирает с `testing`-ребра RCA-класс ORCH-110 (инфра ≠ код-фейл).
**Backward-compat (BR-9):** репо без резолвимого тест-контракта → `applies==False` → прежний
LLM-tester (enduro-trails не затронут).
**Tester-специфичная анти-коллизия (D6.1 локального ADR, отсутствует в ORCH-115):**
`_parse_tests_verdict` читает вердикт из **трёх** полей (`verdict:`/**`status:`**/`result:`) с
negative-token-priority — поэтому обязательное 52c-поле `status:` раннера **жёстко выровнено** по
вердикту (`success` для PASS / `failed` для FAIL), иначе негативный токен в `status:` при `result:
PASS` дал бы ложный FAIL. Зафиксировано unit-тестом через неизменённый парсер.
**Эволюция карты LLM (норматив сопровождения, в том же PR — D12 локального ADR):**
`llm-call-sites.md` (A5 → реализовано детерминированно, но `avoidable=yes`/`axis=C`/
`needs-hybrid-fallback` сохранены — LLM-ветвь как fallback / будущий off-control-path триаж),
`llm-determinization-roadmap.md` (rank 2 tester → реализован; **инвариант «ровно один
`first_slice = yes`» цел** — `first_slice` остаётся у rank 1/deployer, у tester — `no`),
`llm-usage-policy.md` (§5 — транспорт не нарушен), плюс анти-дрейф-тесты
(`test_llm_call_site_inventory.py`/`test_llm_determinization_docs.py`). Эти правки коуплены к коду →
применяются в development атомарно с реализацией, не в architecture-стадии (как ORCH-115).
## Последствия
- **+** Минус ещё один avoidable LLM control path; второй доказанный раннер-паттерн (теперь и для
`needs-hybrid-fallback`-кандидата, не только `replace-deterministic-now`).
- **+** Дешевле/быстрее/детерминированнее собственный `testing`; нет токенов/латентности LLM в точке
ветвления `testing → deploy-staging` / `testing → development`.
- **+** Паттерн остаётся переиспользуемым: leaf + перехват до `_spawn` + `advance_stage` — шаблон для
Phase 2 (project test contract не-self репо + опциональный off-control-path LLM-триаж).
- **+** Гибрид-граница (D11 локального ADR): архитектура не закрывает будущий off-control-path триаж,
не пуская LLM обратно в поток управления вердикта.
- **** Новый компонент + врезка + defer-механика + tester-специфичная анти-коллизия `status:`.
Митигейшн: never-raise leaf, kill-switch (fail-safe к LLM), без схемы БД, инвариант выравнивания
`status:` + структурное покрытие `tests/test_orch116_test_runner.py`.
- **Откат:** `ORCH_TEST_RUNNER_ENABLED=false` → прежний LLM-путь на `testing` байт-в-байт.
## Ссылки
- Локальный ADR: `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`
- Первый срез: [adr-0048](adr-0048-deterministic-staging-runner.md) (ORCH-115, `src/staging_runner.py`)
- Политика/карта/roadmap: [llm-usage-policy.md](../llm-usage-policy.md),
[llm-call-sites.md](../llm-call-sites.md) (A5),
[llm-determinization-roadmap.md](../llm-determinization-roadmap.md) (rank 2),
[adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)
- Прецеденты: D1/D2 (`launcher.py:397/402`), `_run_staging_runner_job` (`launcher.py:438`),
`run_staging_gate` (`staging_runner.py`), `proc_group` (ORCH-110,
[adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md)),
transition-lease (ORCH-114, [adr-0045](adr-0045-transition-ownership-lease-and-stage-cas.md))

View File

@@ -0,0 +1,110 @@
---
work_item: ORCH-124
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---
# ADR-0051: Ось «пауза» serial-gate — park-сигнал без блокировки FIFO
Сквозной (cross-cutting) ADR. Детальное решение задачи —
`docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`.
Статус: **Proposed** · Дата: 2026-06-16 · Источник: **ORCH-124** (bug → escalate full-cycle)
## Контекст
ORCH-088 (serial-gate, adr-0017) определяет «активную задачу репо» **исключительно по машинной стадии**
`tasks.stage NOT IN ('done','cancelled')` (после ORCH-090/adr-0026 — с учётом терминала `cancelled`).
Plane-статусы Backlog/Blocked/Needs-Input — **слой B (индикация), ORCH-066** — не меняют `tasks.stage`
(слой A); у таблицы `tasks` нет колонки статуса. ⇒ приостановленная оператором задача неотличима от
активно исполняемой и держит FIFO-гейт (`t2.id < jobs.task_id`) закрытым для более поздних analyst-job
того же репо.
**Инцидент ORCH-116/ORCH-123:** ORCH-116 поставили на паузу, чтобы пропустить срочный фикс ORCH-123, но
serial-gate держал analyst-job ORCH-123 в `queued`. Единственные обходы (терминальный `cancel`, довод до
`done`, глобальное `serial_gate_enabled=false`) — грубые.
Горячий путь `serial_gate.build_claim_clause` врезан в `claim_next_job`**offline SQL** — и сетевого
чтения Plane-статуса (как делает reconciler ORCH-060) позволить не может. Нужен **DB-резолвимый** сигнал
паузы.
## Решение
### Инвариант: «пауза» — ОТДЕЛЬНАЯ ОСЬ планировщика, ортогональная «терминальности»
Вводится **per-task park-сигнал** — аддитивная нуллабельная колонка **`tasks.paused_at TEXT`**
(NULL = не на паузе) — и **новая ось планировщика «пауза»**, независимая от оси «терминальность».
| Ось | Предикат | Кто использует | Меняется ORCH-124? |
|-----|----------|----------------|--------------------|
| **Терминальность** (adr-0026) | `stage IN ('done','cancelled')` | `serial_gate` + `task_deps` + `stages.py` | **НЕТ — байт-в-байт** |
| **Пауза** (новая, ORCH-124) | `paused_at IS NOT NULL` | **только** FIFO «active» предикат `serial_gate` | да (аддитивно) |
**serial-gate «активная задача» ⇔ `stage NOT IN ('done','cancelled') AND paused_at IS NULL`.** Это
**осознанная, задокументированная** дивергенция serial-gate от чисто-терминального предиката (требование
гармонизации adr-0026): пауза выводит предшественника из FIFO-учёта serial-gate, **не делая его
терминальным**.
### Что НЕ меняется (анти-регресс adr-0026)
- **`task_deps`** (adr-0015) и **`stages.py::STAGE_TRANSITIONS`** колонку `paused_at` **не читают**
остаются чисто терминальными. Явно объявленная зависимость (`job_deps`) на **приостановленную** задачу
**по-прежнему блокирует** зависимый job. Пауза («пропустите меня в FIFO») и dependency («B нужен
результат A») — разные оси; пауза НЕ обходит dependency и НЕ обходит per-repo `repo_freeze`.
- `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схемы существующих таблиц — без
изменений. Пауза — не стадия и не Quality Gate, а признак планировщика очереди.
### Точки, признающие ось «пауза» (исчерпывающе)
1. `src/serial_gate.py::build_claim_clause` — терм `AND t2.paused_at IS NULL` внутри `active_clause`
(под под-флагом). **(маркер ORCH-124, рядом с ORCH-088/ORCH-090)**
2. `src/serial_gate.py::repo_has_active_task` / `_per_repo_snapshot` — тот же предикат + наблюдаемость
(ключ `paused`, `reason` ожидания).
3. `src/db.py` — колонка `tasks.paused_at` (`_ensure_column`) + хелперы `set_task_paused`/
`clear_task_paused`/`is_task_paused`.
4. `src/main.py` — операторские эндпоинты `POST /serial-gate/pause|resume` (по образцу
`POST /serial-gate/unfreeze`).
### Анти-stale-base при возобновлении (ORCH-088 не регрессирует)
Пауза «демотирует» задачу в FIFO; свежесть базы при resume обеспечивают **существующие** механизмы — новой
rebase-машинерии нет: отложенный срез ветки (ORCH-088, для паузнутой-в-`analysis`) + безусловный pre-merge
`auto_rebase_onto_main` под merge-lease (ORCH-026/093) + merge-gate re-test (ORCH-110) для уже
материализованной ветки. Нормальная задача (`paused_at IS NULL`) по-прежнему держит гейт.
### Флаги / совместимость
- Независимый под-флаг `serial_gate_pause_enabled` (env `ORCH_SERIAL_GATE_PAUSE_ENABLED`, дефолт `True`) —
зеркало `serial_gate_freeze_enabled`. `False` ⇒ pause-терм опущен из SQL, эндпоинты no-op ⇒ serial-gate
байт-в-байт как ORCH-088/090. Область — переиспользует `serial_gate_repos` (новый `*_repos` не вводится).
- Дефолт `True` безопасен: пока ни одна задача не на паузе, `paused_at` везде `NULL` ⇒ истинный no-op
(enduro не затронут).
- never-raise: pause-терм в `build_claim_clause` сохраняет **fail-OPEN**; freeze — **fail-CLOSED**.
- Миграция — только аддитивная/идемпотентная (`_ensure_column`); общая прод-БД безопасна (NFR-3).
## Последствия
- **+** Чистая операторская «пауза без блокировки», отличная от cancel (терминал) и от kill-switch;
durable, offline, webhook-независимая; закрывает инцидент ORCH-116/ORCH-123.
- **+** Единый, явно описанный двухосевой предикат планировщика (терминальность ⊥ пауза) — устранён риск
будущего рассинхрона.
- **** Появилась вторая ось «активности» serial-gate — будущие подсистемы планировщика обязаны помнить:
serial-gate «активна» = `не терминальна И не на паузе`, но **терминал** (`task_deps`/`stages.py`) ось
«пауза» НЕ включает. Митигейшн: этот ADR + маркер `ORCH-124` в изменённых местах + тесты.
- **Откат:** `ORCH_SERIAL_GATE_PAUSE_ENABLED=false` (serial-gate 1:1 как ORCH-088/090; колонка `paused_at`
инертна).
## Эволюция маркеров
Горячий SQL serial-gate несёт теперь 3 маркера (`ORCH-088` FIFO-гейт, `ORCH-090` терминал `cancelled`,
`ORCH-124` ось паузы) — правка любого из них сверяется с этим сводным ADR (анти-археология: 3+ маркеров →
одна ссылка сюда, `docs/_standards/TRACEABILITY.md`).
## Ссылки
- Детальный ADR: `docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`
- Данные: `docs/work-items/ORCH-124/08-data-requirements.md`
- Связанные: adr-0017 (serial-gate ORCH-088), adr-0026 (терминал `{done,cancelled}` ORCH-090),
adr-0015 (task-deps), adr-0027 (merge-актор rebase/retry ORCH-093), adr-0042 (merge-gate re-test ORCH-110)

View File

@@ -0,0 +1,99 @@
---
work_item: ORCH-126
stage: architecture
author_agent: architect
status: accepted
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# adr-0052: Инвариант run-ownership строки `jobs` — «queued ⇒ run_id/pid/started_at IS NULL»
- **Статус:** accepted
- **Дата:** 2026-06-17
- **Задача:** ORCH-126 (bug-fix контрол-плейна)
- **Детальный ADR:** `docs/work-items/ORCH-126/06-adr/ADR-001-queued-job-run-ownership-hygiene.md`
## Контекст
Колонки `jobs.run_id` и `jobs.pid`**общий контракт liveness/идентичности run'а**, на который
опираются несколько подсистем контрол-плейна:
- **job-reaper (ORCH-065, adr-0011/adr-0043):** Tier-1 судит liveness running-job'а по `jobs.pid`
(`merge_gate.pid_alive`);
- **`/metrics` (ORCH-099, adr-0030):** `get_running_agents` отдаёт `run_id`/`pid` running-job'ов
как «сырьё» для sidecar;
- **scheduler/launcher (ORCH-1/ORCH-088):** `_spawn` выставляет `run_id` (после INSERT в `agent_runs`)
и `pid` (после `Popen`) **вперёд**.
Но ни один путь возврата job'а в `queued` (restart-recovery `requeue_running_jobs`,
retry `mark_job('queued')`, transient `mark_job_transient`, reaper `reap_running_job('queued')`) не
сбрасывал run-ownership — он оставался «протухшим» от прошлой попытки. Возникало физически невозможное
состояние `status='queued'` с непустыми `run_id`/`pid` при `started_at IS NULL`. Поскольку pid после
рестарта контейнера может быть **переиспользован** ОС, `pid_alive(stale)` ложно возвращает `True`,
reaper видит «живой» фантомный `running` и при `max_concurrency=1` (дефолт) клинит клейм **всей**
очереди — а это **общий** инстанс/очередь всех проктов (self-hosting). Инцидент ORCH-124/125: queued
analyst-job'ы зависали навсегда даже при `ORCH_SERIAL_GATE_ENABLED=false`.
Корень — **отсутствие именованного, принудительно соблюдаемого инварианта**, связывающего
`jobs.status` с его run-ownership-колонками.
## Решение
Зафиксировать как **системный инвариант данных контрол-плейна**:
> **`status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`.**
То есть: **queued-job никогда не несёт run-ownership.** Run-ownership принадлежит ровно одной активной
попытке (`running` после стампа в `_spawn`) и история живёт в таблице `agent_runs`, а не в
`jobs.run_id`.
Соблюдение (ORCH-126, без смены схемы БД, на существующих колонках):
- **Forward-cleanup:** каждый путь перехода в `queued` выставляет `run_id=NULL, pid=NULL` той же
UPDATE-транзакцией, что чистит `started_at`/`finished_at` (атомарные `status`-guard'ы сохранены).
- **Clean claim (defense-in-depth):** `claim_next_job` при флипе `queued→running` сбрасывает stale
`pid`/`run_id` тем же UPDATE — между claim и стампом `pid` в `_spawn` строка несёт `pid IS NULL`,
не чужой pid (offline hot-path не трогается).
- **Self-heal + наблюдаемость:** «невозможные» queued-строки санируются идемпотентно при старте/реапе
(never-raise) и видны счётчиком в `GET /queue` — защита от рецидива, если будущий путь возврата в
`queued` забудет инвариант.
**Норматив на будущее:** любой новый путь, переводящий job в `queued`, **обязан** соблюсти инвариант
(сбросить `run_id`/`pid`). Reviewer ловит нарушение как ≥P1 (фантомный `running` способен заклинить
очередь всех проектов).
`STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи / **схема БД**
байт-в-байт. Это инвариант данных планировщика, **не** Quality Gate и **не** стадия.
## Альтернативы
- **DB-level CHECK/триггер** — отвергнуто: смена схемы; раняющий констрейнт нарушает never-raise и мог
бы заклинить очередь всех проектов. Инвариант лучше держать кодом + self-heal, чем раняющим
констрейнтом.
- **Reaper-side эвристика поверх stale pid** — отвергнуто: лечит симптом у одного читателя, оставляет
stale-данные другим (`/metrics`); reaper уже корректно трактует `pid IS NULL`.
- **Новая колонка-эпоха run'а** — отвергнуто: смена схемы, избыточно; инвариант выразим на
существующих колонках.
## Последствия
- Класс «фантомный `running` клинит `max_concurrency=1`-очередь всех проектов» закрыт у корня;
восстановлена корректность Tier-1 reaper-liveness; чище `/metrics`.
- Инвариант **назван** → перестаёт быть «неявным предположением» reaper'а/metrics и становится
проверяемым контрактом (reviewer + self-heal).
- Нулевая регрессия для здоровых job'ов и enduro-trails; миграция БД не требуется (аномальные строки
санируются при первом старте).
- Аддитивно/обратимо: **не** `arch:major-change` (нет новой стадии / QG / таблицы / смены топологии).
- **Откат:** ревертом ORCH-126 PR; опц. self-heal/диагностика — своим флагом.
## Связи
- adr-0011 / `docs/work-items/ORCH-065/06-adr/` (job-reaper Tier-1 по `jobs.pid` — читатель инварианта;
фикс восстанавливает его предусловие).
- adr-0043 / `docs/work-items/ORCH-113/06-adr/` (finalizer-liveness — ортогонален: process-local,
по `job_id`).
- adr-0045 / `docs/work-items/ORCH-114/06-adr/` (transition-lease — ортогонален: своя таблица/колонки,
recovery по boot-id).
- adr-0030 / `docs/work-items/ORCH-099/06-adr/` (`/metrics` `get_running_agents` — читатель `pid`/
`run_id`; уже допускает `pid IS NULL`).
- adr-0002 (job-queue ORCH-1 — порождающая модель `jobs`).
</content>

View File

@@ -0,0 +1,82 @@
---
work_item: ORCH-120
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# ADR-0053: Поток «открытые вопросы аналитика → Needs Input» (приоритет + пауза + resume)
Сквозной (cross-cutting) ADR. Детальное решение задачи —
`docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`.
Статус: **Proposed** · Дата: 2026-06-17 · Источник: **ORCH-120** (bug → escalate full-cycle)
## Контекст
Конвейер обязывает аналитика выпустить 4 файла (`01-brd`/`02-trz`/`03-acceptance-criteria`/
`04-test-plan.yaml`), иначе exit-гейт `analysis` не пройдёт. При неоднозначном бизнес-запросе
(классика — `Description: TBD`) у аналитика нет рабочего канала уточнения → он **фабрикует**
требования. Механизм «вопросы → Needs Input» в `_handle_analysis_approved_flow`
(`src/stage_engine.py`) **существует, но мёртв** из-за четырёх смежных дефектов: контракт не
доведён до промпта; ветка `files_ok` имеет приоритет над веткой вопросов; Needs Input клинит
serial-gate репо (ORCH-088); нет гигиены устаревшего `01-questions.md`.
Поток пересекает несколько подсистем, поэтому фиксируется сквозным ADR (анти-археология ORCH-078:
блок `_handle_analysis_approved_flow` несёт 3+ маркера — ORCH-066/088/089/124):
- **ORCH-066** — Needs Input принадлежит **только** аналитику (слой B индикации ≠ слой A стадий).
- **ORCH-088** — per-repo serial-gate: «активная задача» по `tasks.stage NOT IN ('done','cancelled')`.
- **ORCH-124** (adr-0051) — ортогональная ось «пауза» (`tasks.paused_at`): исключает задачу из
«активного» предиката, **не** обходя оси `task_deps`/`repo_freeze`/терминал.
- **ORCH-089** — autoApprove (человеческий BRD-гейт по лейблу) в той же ветке `files_ok`.
## Решение
**Активировать мёртвый путь четырьмя согласованными изменениями** — аддитивно, под kill-switch,
скоуп self-hosting, never-raise:
1. **Контракт промпта + канон артефакта.** `.openclaw/agents/analyst.md` документирует канал
«блокирующие вопросы → `01-questions.md`, НЕ фабриковать deliverables»; `01-questions.md`
стандартизирован как **сигнальный** when-applicable артефакт (скелет `docs/_templates/` +
строка манифеста `PIPELINE_DOCS.md`) — **не** machine-verdict (гейтом не парсится, BR-6).
2. **Приоритет «вопросы активны» > «файлы готовы».** В `_handle_analysis_approved_flow` предикат
активных вопросов проверяется **до** ветки `files_ok` → блокирующие вопросы надёжно достигают
Needs Input даже при частичных/сфабрикованных deliverables.
3. **Авто-park через ось «пауза» ORCH-124.** Переход в Needs Input вызывает `db.set_task_paused`
→ задача исключается из «активного» предиката serial-gate → следующая задача репо входит в
`analysis`, пока первая ждёт человека (не клинит FIFO неопределённо долго).
4. **Resume + unpark.** `handle_status_start` (analysis-resume) снимает паузу (`clear_task_paused`)
и перезапускает аналитика; relaunch-guard ORCH-090 (только `analysis`) не ослаблен.
**Устаревание `01-questions.md` (детерминированно, offline):** freshness-gated supersede по mtime —
вопросы «активны», пока пакет неполон ИЛИ `01-questions.md` не старше всех 4 deliverables; полный
свежий пакет supersedeит старый файл (выбор механизма и отвергнутые альтернативы — ADR-001 DQ-2).
## Инварианты (нормативно)
- **Поток — pre-gate-ветка движка, НЕ Quality Gate.** `STAGE_TRANSITIONS` / реестр и имена
`QG_CHECKS` / семантика `check_analysis_complete`/`check_analysis_approved` / machine-verdict-ключи
/ схемы существующих таблиц — **байт-в-байт не тронуты**.
- **Без схемы БД:** переиспользуется `tasks.paused_at` (ORCH-124); новых таблиц/колонок нет.
- **ORCH-066 не расширяется:** Needs Input остаётся **только** у аналитика.
- **ORCH-124 не регрессирует:** пауза ортогональна — оси `task_deps`/`repo_freeze`/терминал
`{done,cancelled}` `paused_at` **не читают**; анти-stale-base ORCH-088 цел (нормальная задача
`paused_at IS NULL` держит гейт; свежесть базы на resume — существующими механизмами).
- **Self-hosting-безопасность:** поток только меняет Plane-статус/паузу/коммент и читает worktree —
не деплоит, не рестартит прод-контейнер, не пушит в `main`, не трогает detached-процессы.
- **never-raise / обратимость:** все врезки изолированы и деградируют к прежнему поведению;
3 флага (`analyst_questions_gate_enabled` / `analyst_questions_gate_repos` /
`analyst_needs_input_autopause_enabled`) с безопасными дефолтами → off/out-of-scope = байт-в-байт
как до ORCH-120 (enduro не затронут).
## Последствия
Конвейер перестаёт строить решения поверх домыслов; serial-gate не клинит на задаче, ждущей
человека (поддержка автономного пакетного прогона ORCH-088); аналитик получает легитимный канал
уточнения. Цена — узкое связывание индикации с осью планировщика при авто-park (смягчено флагом +
узким триггером + never-raise) и зависимость supersede от mtime (смягчено: полный прогон всегда
пишет свежие deliverables + контракт промпта). Детали, альтернативы и риски —
`docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`,
`docs/work-items/ORCH-120/10-tech-risks.md`.

View File

@@ -0,0 +1,94 @@
---
work_item: ORCH-020
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# ADR-0054: Оценка задачи — операторский статус-триггер «Оценка» + детерминированная эвристика по истории
Сквозной (cross-cutting) ADR. Детальное решение задачи —
`docs/work-items/ORCH-020/06-adr/ADR-001-task-estimation-status-trigger.md`.
Статус: **Proposed** · Дата: 2026-06-17 · Источник: **ORCH-020**
## Контекст
Заказчик планирует бэклог вручную и хочет видеть прогноз **стоимости / времени / токенов / сложности
(story points `{1,2,3,5,8}`)** до отправки задачи в работу. Ключевое требование триггера (после REJECT
2026-06-17): оценка — **операторский жест в Plane** (перевод issue в выделенный статус «Оценка»,
**массово** через multi-select), а не невидимый авто-шаг на `start_pipeline`. Шаг 2 (адаптивный выбор
модели) — **вне объёма**.
Решение пересекает несколько подсистем (webhook-роутинг, `plane_sync`, БД, `notifications`, `stage_engine`
done-хук), поэтому фиксируется сквозным ADR. Опирается на установленные инварианты платформы:
- **Семейство операторских action-статусов** STOP (ORCH-090) / Confirm Deploy (ORCH-059): fail-closed
`.get("<key>")`-ветка в `handle_issue_updated`, ключ **намеренно отсутствует** в `_DEFAULT_STATES`.
- **Write-guard ORCH-117** — все записи в Plane изолированы от тест/worktree-процессов.
- **leaf-паттерн** (serial_gate/coverage_gate/bug_fast_track/lessons): never-raise, kill-switch, скоуп
`*_repos` (пусто → self-hosting only), read-only блок в `GET /queue`.
- **determinization-политика ORCH-118** (`llm-usage-policy.md`): не вводить avoidable LLM-пути.
## Решение
**Вводим третий член семейства action-статусов — «Оценка» — как side-механизм, делегирующий новому leaf
`src/estimator.py`.** Механизм прогноза — **детерминированная эвристика по истории** (чистые функции, без
LLM-вызова). Аддитивно, под kill-switch, скоуп self-hosting, never-raise, fail-safe:
1. **Триггер (D4).** `_PLANE_NAME_TO_KEY["Оценка"]="estimate"` (НЕ в `_DEFAULT_STATES`); fail-closed ветка
`proj_states.get("estimate")``handle_estimate` (off-loop `to_thread`, зеркало `handle_stop`).
Взаимоисключение жестов — по различию UUID статусов, не по порядку.
2. **Анти-disruption + авто-возврат + анти-loop (D5).** Guard `applies(repo)` ПЕРВЫМ (локально) +
`has_active_job_for_task` (активный job → no-op, не выдёргивать in-flight). После оценки —
`set_issue_backlog`; `backlog` не совпадает ни с одной триггер-веткой → возврат = no-op-эхо.
3. **Механизм прогноза (D1/D2/D3).** **Без LLM:** прогноз = средние токены/время/стоимость похожих
`done`-задач (`repo` + `track` ORCH-019, через read-only `usage.py`-агрегаты), bootstrap при пустой
истории; story-points — чистая функция-бакетизатор по `forecast_cost_usd` с конфигурируемыми порогами.
4. **Запись в Plane (D6).** Прогноз story-points → `set_issue_estimate_point` (FK на estimate-point,
резолв `value→uuid`); факт → `set_issue_point` (устойчивый int); коммент → `add_comment`. Все под
`_guard_allows_write` (ORCH-117); отсутствие estimate-системы → best-effort пропуск + лог (NFR-7).
5. **Персистентность (D7).** Новая аддитивная таблица `task_estimates` (`UNIQUE(work_item_id)`,
UPSERT-идемпотентность пере-оценки; `task_id` нуллабелен — issue на бэклоге). Фундамент петли калибровки
(ORCH-8).
6. **Поверхности (D8).** Пункт «Оценка» (время·токены·стоимость) в общей Telegram-карточке
(`notifications`, never-raise, ORCH-087/095-совместимо).
7. **Факт на `done` (D9).** Best-effort врезка в `stage_engine.advance_stage` (блок `next_stage=="done"`,
после terminal-sync): факт из `usage.py``set_actual` + `set_issue_point`; `estimate_point` не
перезаписывается.
**Главное архитектурное решение — отказ от LLM-оценщика (D1).** Причины: NFR-5 (massive multi-select
умножил бы LLM-вызовы и конкурировал бы за единственный транспорт `launcher._spawn` с боевыми агентами,
рискуя обслуживанием enduro), NFR-4 (стоимость самой оценки), и политика ORCH-118 (размер задачи
деривируем из tool-сигналов — суждение LLM не требуется). Контракт `estimate()` — граница расширения под
будущий гибрид без переписывания вызывающих (но он сейчас НЕ строится).
## Инварианты (нормативно)
- **Оценка — наблюдатель/продюсер, НЕ Quality Gate и НЕ переход стадии.** `STAGE_TRANSITIONS` / реестр и
имена `QG_CHECKS` / семантика `check_*` / machine-verdict-ключи (`verdict:`/`result:`/`deploy_status:`/
`staging_status:`/`security_status:`/`coverage_status:`) / **схемы существующих таблиц** — **байт-в-байт
не тронуты**. Статус «Оценка» не добавляет ребра в машину стадий.
- **Горячий путь не тронут:** `resolve_agent_model`/`resolve_agent_effort`/`_spawn` без изменений (Шаг 2
вне объёма — отдельный work item с зависимостью на ORCH-13).
- **Схема БД:** ровно **одна** аддитивная таблица `task_estimates` (`CREATE TABLE IF NOT EXISTS`);
существующие таблицы не изменяются (NFR-8). Hot-path `claim_next_job`/очередь её не читают.
- **Self-hosting-безопасность:** модуль только читает/пишет свою таблицу, читает usage-агрегаты и пишет в
Plane/Telegram — не деплоит, не рестартит прод-контейнер, не трогает `main`/force-push, без процессов.
- **never-raise / обратимость:** все публичные функции и врезки изолированы; `estimator_enabled=false` /
доска без статуса «Оценка» / репо вне `estimator_repos` → байт-в-байт как до ORCH-020 (enduro и текущий
orchestrator не затронуты).
- **Без kill-switch обхода write-guard:** записи `estimate_point`/`point`/коммент/состояние подчиняются
ORCH-117 (anti-drift: тест-процесс физически не пишет в боевой Plane).
## Последствия
Оператор получает прогноз для планирования бэклога одним массовым жестом; пере-оценка идемпотентна;
заложен леджер прогноз↔факт под петлю калибровки (ORCH-8). Цена — net-new интеграция с Plane-estimate API
(`estimate_point` — FK; смягчено best-effort/fail-safe + устойчивым int-`point`) и начальные пороги
story-points (смягчено конфигурируемостью + леджером). Решение сознательно консервативно (детерминировано,
обратимо, согласовано с ORCH-118) и **не требует** `arch:major-change` (аддитивный leaf по устоявшемуся
паттерну, без новой стадии/правки таблиц/смены БД). Детали, альтернативы и риски —
`docs/work-items/ORCH-020/06-adr/ADR-001-task-estimation-status-trigger.md`,
`docs/work-items/ORCH-020/07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`.

View File

@@ -70,6 +70,14 @@ STAGE_TRANSITIONS = {
рёбер не меняются), а терминал STOP-отмены. Системный предикат «задача завершена» —
`stage ∈ {done, cancelled}` (синхронно в `reconciler`/`serial_gate`/`task_deps`; adr-0026).
**Ось «пауза» ⊥ оси «терминальность» (ORCH-124, adr-0051):** serial-gate вводит **отдельную** ось
паузы `tasks.paused_at IS NOT NULL` (durable per-task park-сигнал) — **ортогональную** терминалу. Для
serial-gate «активная задача» ⇔ `stage NOT IN ('done','cancelled') AND paused_at IS NULL` (паузнутый
предшественник не держит FIFO). **Терминал `{done,cancelled}` НЕ расширяется паузой:** `task_deps` и
`stages.py` колонку `paused_at` НЕ читают (паузнутая объявленная зависимость по-прежнему блокирует
зависимый job; пауза не обходит `repo_freeze`). Пауза — признак планировщика очереди, не стадия и не
терминальное состояние.
### 3. Quality Gates (`src/qg/checks.py`)
| Check | Метод проверки |
@@ -96,6 +104,33 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
3. Стартует **watchdog thread** (per-role wall-clock бюджет → SIGTERM→grace→SIGKILL по pid; ORCH-109: developer 60 мин / reviewer 50 мин / прочие 30 мин дефолт, `_resolve_timeout`)
4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance
**Детерминированные перехваты `launch_job` ДО `_spawn` (no-LLM джобы).** Перед `_spawn` `launch_job`
перехватывает зарезервированные роли и исполняет их детерминированно, сам ведя `jobs`-строку через
`mark_job` (нет `agent_runs`, нет токенов): `deploy-finalizer` (D1, ORCH-036 Phase C) и
`post-deploy-monitor` (D2, ORCH-021). **ORCH-115 ([adr-0048](adr/adr-0048-deterministic-staging-runner.md)):**
тем же паттерном перехватывается джоб `deployer` на стадии `deploy-staging` для in-scope репо
(дискриминатор — **стадия задачи**, не имя роли: роль `deployer` общая для `deploy-staging`/`deploy`;
+`staging_runner.applies(repo)` под kill-switch `staging_runner_enabled`, скоуп `staging_runner_repos`,
пусто → self-hosting only; `should_intercept` never-raise → `False` → штатный `_spawn`, fail-safe к
LLM). Leaf `src/staging_runner.py` (зеркало `run_deploy_finalizer`) исполняет staging-сюиту через
`proc_group` (tree-kill, таймаут `staging_runner_timeout_s`), маппит exit-код
`self_deploy.map_exit_code_to_status`, пишет `15-staging-log.md` (тот же machine-key `staging_status:`)
и вызывает существующий `advance_stage(finished_agent="deployer")` (см. §5). Так LLM-агент `deployer`
на `deploy-staging` исчезает из happy-path; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты.
**ORCH-116 ([adr-0050](adr/adr-0050-deterministic-test-runner.md)):** тем же паттерном (рядом с
ORCH-115) перехватывается джоб `tester` на стадии `testing` для in-scope репо с тест-контрактом
(дискриминатор — роль `tester` **И** `tasks.stage == "testing"` **И** `test_runner.applies(repo)` под
kill-switch `test_runner_enabled`, скоуп `test_runner_repos`, резолв `_has_test_contract`; пусто →
self-hosting only; `should_intercept` never-raise → `False` → штатный `_spawn`, fail-safe к LLM). Leaf
`src/test_runner.py` (зеркало `run_staging_gate`) исполняет регресс `pytest <target>` **в worktree
ветки** через `proc_group` (tree-kill, таймаут `test_runner_timeout_s`) + read-only smoke, маппит
exit-код `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе→`FAIL`), пишет
`13-test-report.md` (тот же machine-key `result:`; 52c-`status:` выровнен по вердикту — D6.1) и вызывает
существующий `advance_stage(finished_agent="tester")` (см. §5). Двухуровневый исход (анти-ORCH-110):
сюита НЕ исполнилась → bounded defer re-queue **`tester`**-джоба, не код-фейл-откат. Так LLM-агент
`tester` на `testing` исчезает из happy-path; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_tests_passed`/схема
БД не тронуты.
### 5. Auto-advance (`launcher._try_advance_stage`)
После успешного завершения агента:
@@ -188,6 +223,21 @@ CREATE TABLE events (
payload TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ORCH-114 (adr-0045): durable transition-ownership lease. ОДНА аддитивная таблица
-- (CREATE TABLE IF NOT EXISTS, паттерн repo_freeze/coverage_baseline/lessons) — одна
-- строка = ≤1 активный владелец side-effectful перехода задачи. Живость владельца =
-- owner_boot_id (нонс старта процесса; рестарт ⇒ смена ⇒ прежний lease мёртв) +
-- pid_alive(owner_pid). БЕЗ epoch/version-колонки на tasks (стадия = версия CAS).
CREATE TABLE transition_lease (
task_id INTEGER PRIMARY KEY,
owner TEXT NOT NULL, -- monitor|reaper|reconciler|webhook|finalizer|engine
owner_pid INTEGER,
owner_boot_id TEXT,
run_id INTEGER,
stage TEXT, -- from-стадия захвата (контекст/наблюдаемость)
acquired_at TEXT DEFAULT (datetime('now'))
);
```
## Deployment
@@ -352,8 +402,8 @@ webhook (plane/gitea) background thread (queue_worker)
|--------|------------|
| `status` | `queued``running``done` \| `failed` \| `cancelled` (ORCH-090: терминальный исход STOP-отмены, не реквью'ится) |
| `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) |
| `run_id` | FK на `agent_runs.id` после старта. **ORCH-126 (adr-0052):** run-ownership; `queued ⇒ run_id IS NULL` (история run'а живёт в `agent_runs`, не в `jobs.run_id`) |
| `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent). **ORCH-126 (adr-0052):** `queued ⇒ pid IS NULL` — иначе протухший (возможно переиспользованный ОС) pid ложно «оживает» в Tier-1 reaper и клинит очередь |
| `task_content` | ТЗ, которое пишется в task-файл агента |
| `error` | последняя ошибка |
@@ -369,7 +419,17 @@ status='queued'` и проверяет `rowcount`. При гонке двух т
В `main.py` lifespan **после** M-1 orphan-recovery вызывается `requeue_running_jobs()`:
jobs со статусом `running` (воркер умёр на рестарте) → возвращаются в `queued`.
Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
**ORCH-126 (adr-0052):** возврат в `queued` сбрасывает run-ownership (`run_id=NULL, pid=NULL`
вместе с `started_at`) — мёртвый воркер оставил их протухшими, и фантомный pid заклинил бы
Tier-1 reaper. Сразу следом `reaper.sanitize_impossible_queued_once()` идемпотентно санирует
любые «невозможные» queued-строки (`queued` с непустым `run_id`/`pid`/`started_at`).
**ORCH-114 (adr-0045):** сразу следом вызывается `transition_lease.recover_on_startup()`
новый процесс имеет свежий `boot_id`, поэтому ВСЕ записанные ранее `transition_lease`
устарели (boot-id mismatch) → реклеймятся, и только что requeued-jobs переисполняют свои
side-effectful переходы **последовательно** (один владелец), без двойного необратимого
эффекта. Идемпотентность самого re-drive обеспечивают существующие авторитетные факты
(SHA-in-main ORCH-071/073, маркер `INITIATED` ORCH-036, coverage-ratchet CAS ORCH-027) —
НЕ новый recovery-мозг. Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
### Job-reaper (ORCH-065, рестарт НЕ требуется)
@@ -385,7 +445,25 @@ daemon-поток `src/job_reaper.py` (каркас `reconciler`) периоди
git push/PR/Plane-комментарии (секунды-десятки секунд) и лишь потом
`_finalize_job`; pid агента к этому моменту мёртв в обоих случаях. Поэтому
Tier-2 реапит только после finalization-grace `reaper_finalize_grace_s`
(`finished_age_s >= grace`) — живой финализирующий monitor НЕ реапится;
(`finished_age_s >= grace`) — живой финализирующий monitor НЕ реапится.
**ORCH-113 (adr-0043):** на ребре `deploy-staging → deploy` финализация длится
**минуты** (тяжёлые edge-под-гейты после штампа `finished_at`, до `_finalize_job`),
grace=300 это не покрывал → живой долгий finalizer ошибочно реапился и повторял
advance (ложный откат, инцидент ORCH-111). Tier-2 консультирует процесс-локальный
реестр владения `src/finalizer_liveness.py` (`mark`/`clear` в потоке монитора через
try/finally): при `stage=="deploy-staging"` И активном владении → **defer**;
Tier-3 backstop маркер игнорирует (мёртвый/застрявший finalizer добивается).
Kill-switch `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`; in-memory, restart-safe через
`requeue_running_jobs` (до старта reaper); схема БД и сквозной бюджет не тронуты.
**ORCH-114 (adr-0045):** обобщает это in-memory-владение до **durable, кросс-путевого**
`transition_lease` (таблица `task_id PK, owner, owner_pid, owner_boot_id, …`): reaper
консультирует durable-lease на **всех** релевантных путях (не только Tier-2/`deploy-staging`),
живость владельца = `pid_alive(owner_pid)` + совпадение boot-id (рестарт ⇒ прежние lease мертвы);
парная CAS-запись стадии (`update_task_stage_cas`, `WHERE id=? AND stage=?`) — аборт проигравшего
без побочных эффектов; reconciler F-1 и webhook тоже defer при живом владельце. Kill-switch
`ORCH_TRANSITION_LEASE_ENABLED` (off → ровно поведение ORCH-113 выше); `finalizer_liveness.py`
не правится (надстройка durable-слоя поверх). Потолок возраста lease = `reaper_max_running_s`
(Tier-3 force-освобождает), сквозной бюджет цел;
- **Tier-3** — backstop: job висит `running` дольше `reaper_max_running_s`.
Реап атомарен (`UPDATE jobs SET ... WHERE id=? AND status='running'` + `rowcount`,
@@ -401,6 +479,35 @@ claim делает `_try_advance_stage` (advance+enqueue) — проигравш
/ `ORCH_LEASE_RECLAIM_ENABLED`; снимок в `GET /queue` (блок `reaper`). Подробнее —
adr-0011.
### Инвариант run-ownership строки `jobs` (ORCH-126, adr-0052)
Колонки `jobs.run_id`/`jobs.pid`**общий контракт liveness/идентичности run'а** (читают
job-reaper Tier-1 по `pid`, `/metrics` `get_running_agents`). Системный инвариант данных:
> **`status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`.**
То есть **queued-job никогда не несёт run-ownership** — оно принадлежит ровно одной активной
попытке (`running` после стампа в `_spawn`). Корень дефекта (инцидент ORCH-124/125, job 2286
`queued + run_id=759 + pid=35 + started_at=NULL`): ни один путь возврата в `queued` не сбрасывал
run-ownership, а после рестарта контейнера pid мог быть **переиспользован** ОС`pid_alive(stale)`
ложно `True` → reaper «видел живой» фантомный `running` и при `max_concurrency=1` клинил клейм
**всей** общей очереди. Соблюдение (без смены схемы БД):
- **Forward-cleanup** — каждый путь перехода в `queued` (`requeue_running_jobs`,
`mark_job('queued')`, `mark_job_transient`, `reap_running_job('queued')`) выставляет
`run_id=NULL, pid=NULL` той же UPDATE-транзакцией, что чистит `started_at` (атомарные
`status`-guard'ы сохранены). Безусловно (исправление инварианта данных, без флага).
- **Clean claim (defense-in-depth)** — `claim_next_job` при флипе `queued→running` сбрасывает
stale `pid`/`run_id` тем же UPDATE → между claim и стампом `pid` в `_spawn` строка несёт
`pid IS NULL`. SELECT-гейт не тронут (offline hot-path).
- **Self-heal + наблюдаемость** — `db.sanitize_impossible_queued()` идемпотентно санирует
«невозможные» queued-строки при старте (`main.lifespan`) и на каждом реап-тике (never-raise,
kill-switch `ORCH_IMPOSSIBLE_QUEUED_SANITIZE_ENABLED`, дефолт on); счётчик
`impossible_queued_total` в блоке `reaper` снимка `GET /queue`.
**Норматив:** любой новый путь возврата job в `queued` ОБЯЗАН соблюсти инвариант (сбросить
`run_id`/`pid`); reviewer ловит нарушение как ≥P1. Подробнее — adr-0052,
`docs/work-items/ORCH-126/06-adr/ADR-001-queued-job-run-ownership-hygiene.md`.
### Конфиг
- `ORCH_MAX_CONCURRENCY` (default 1) — лимит параллельных jobs.

View File

@@ -0,0 +1,168 @@
# LLM call-site map — inventory, control-path axis & classification (ORCH-118)
> **Что это.** Доказательная карта **каждого места**, где control-path оркестратора
> потребляет (или способен потребить) суждение LLM. Единица инвентаря — **LLM-консультация**
> (control-path потребляет суждение LLM), **не** «спавн процесса / существование Claude CLI»
> (capability ≠ consultation, BRD §0 / R4).
>
> **Снимок, прибитый к коду.** Карта — *снимок*; её инварианты держат структурные тесты
> `tests/test_llm_call_site_inventory.py` (анти-дрейф). Меняешь место вызова LLM или потребителя
> вердикта в `src/qg/checks.py` → **обнови эту карту и политику в том же PR** (норматив сопровождения).
>
> **Источник истины** содержательной классификации — ADR
> `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`
> (D2/D3/D4) и сквозной `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`.
> Нормативное определение «avoidable LLM control path» и критерии keep/replace — в
> [`llm-usage-policy.md`](llm-usage-policy.md); порядок замен — в
> [`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).
---
## 0. Три ортогональных факта (как читать карту)
Карта **явно** разводит три раздельных факта — их смешение было корнем блокеров R3→R5:
1. **Ось 1 — consultation ≠ transport/slot.** «LLM-консультация» = точка, где решение/артефакт
конвейера **потребляет суждение LLM**. Транспорт (`_spawn`) — реализация, не определение. Слот
агента (job-роли `D1`/`D2`) делает site LLM-**capable**, но консультация гейтится потоком
управления (перехват **до** `_spawn`) → **capability ≠ consultation**.
2. **Ось 2 — control-path (C) ≠ artifact-producer (P).** Определяется **кодом-потребителем** вывода
роли в `src/qg/checks.py`:
- **(C) control-path** — LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт**
(PASS → дальше / FAIL → откат). Суждение LLM **входит** в поток управления.
- **(P) artifact-producer** — LLM производит артефакт, а продвижение решает **детерминированный
гейт**, судящий артефакт **независимо** (наличие файлов / CI-статус). Суждение LLM в control
flow **не входит**.
3. **Ось 3 — деривируемость вердикта.** Вердикт C-консультации либо есть **детерминированная функция
tool-сигналов** (exit-code `pytest`/smoke/`staging_check.py`/деплоя), которые оркестратор **уже
вычисляет сам**, либо требует **настоящего суждения**, не сводимого к exit-коду.
> **Avoidable LLM control path** (нормативное определение — [`llm-usage-policy.md`](llm-usage-policy.md)):
> call-site, для которого выполнены **оба** условия — **(i)** это C-консультация (её LLM-вердикт
> потребляется потоком управления) **и** **(ii)** вердикт **деривируем** из tool-сигналов. Тогда
> суждение LLM не добавляет информации → консультацию можно снять без потери смысла.
---
## 1. Инвентарь LLM-консультаций (полный, привязан к коду)
Каждая запись несёт: `id` · `location (file:line)` · `trigger` · `stage/owner` · `output artifact` ·
`machine-verdict key` · `output consumer` (кто потребляет вывод роли) · `est. tokens/runtime`
(оценка из `agent_runs`) · `consults-LLM` · `axis` (C/P) · `classification` · `rationale`.
| id | location (file:line) | trigger | stage / owner | output artifact | machine-verdict key | output consumer (`src/qg/checks.py`) | est. tokens/runtime (оценка) | consults-LLM | axis | classification | rationale |
|----|----------------------|---------|---------------|-----------------|---------------------|--------------------------------------|------------------------------|--------------|------|----------------|-----------|
| **S0** | `src/agents/launcher.py:472` (`_spawn`; CLI-сборка `610-614`; парс токенов `_monitor_agent:838`) | `launch_job``_spawn` для любой из 6 ролей | — (транспорт) | — | — | — | — | **транспорт** (capability) | — | — | Единственный транспорт LLM-консультации в `src/**`; не call-site решения |
| **A1** | `.openclaw/agents/analyst.md` | стадия `analysis` | analyst | `01-brd``04-test-plan` | — | `check_analysis_complete:33` (наличие файлов) | ~80200k / 520 мин | да (через S0) | **P** | `keep-LLM` | анализ требований / BRD/ТЗ — настоящее суждение; гейт судит лишь наличие артефактов |
| **A2** | `.openclaw/agents/architect.md` | стадия `architecture` | architect | `06-adr/`, `07` | — | `check_architecture_done:62` (наличие 06-adr/07) | ~80200k / 520 мин | да (через S0) | **P** | `keep-LLM` | архитектурное решение / ADR — настоящее суждение |
| **A3** | `.openclaw/agents/developer.md` | стадия `development` | developer | код + PR | — | `check_ci_green:82` (+ `check_branch_mergeable:657`) — CI/merge | ~150400k / 1040 мин | да (через S0) | **P** | `keep-LLM` | написание кода — настоящее суждение; гейт судит CI/merge, не самоотчёт |
| **A4** | `.openclaw/agents/reviewer.md` | стадия `review` | reviewer | `12-review.md` | `verdict:` | `check_reviewer_verdict:336` (`verdict:`) | ~100300k / 525 мин | да (через S0) | **C** | `keep-LLM` | control path, но вердикт «приемлемость кода/решения» **НЕ деривируем** из exit-кода — настоящее суждение |
| **A5** | `.openclaw/agents/tester.md` | стадия `testing` | tester | `13-test-report.md` | `result:` | `check_tests_passed:182``_parse_tests_verdict:226` (`result:`) | ~60150k / 520 мин | да (через S0; для in-scope репо на `testing`**нет**, перехват до `_spawn`) | **C** | `needs-hybrid-fallback` | **avoidable, СРЕЗ РЕАЛИЗОВАН (ORCH-116):** на `testing` для self-hosting `orchestrator` (репо с тест-контрактом) вердикт `result:` производит детерминированный `src/test_runner.py` (перехват в `launch_job` до `_spawn`, как D1/D2) — exit-код `pytest` в worktree + read-only smoke. LLM-ветвь остаётся **fallback'ом** под выключенным флагом / для репо без контракта / как будущий **off-control-path** триаж падений (маппинг TC↔критерии), который НЕ выносит и НЕ переопределяет `result:` (гибрид-природа `needs-hybrid-fallback` сохранена) |
| **A6** | `.openclaw/agents/deployer.md` | стадии `deploy-staging` / `deploy` | deployer | `15-staging-log.md` / `14-deploy-log.md` | `staging_status:` / `deploy_status:` | `check_staging_status:599``_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473``_parse_deploy_status:413` (`deploy_status:`) | ~40120k / 315 мин | да (через S0; для in-scope репо на `deploy-staging`**нет**, перехват до `_spawn`) | **C** | `replace-deterministic-now` | **avoidable, СРЕЗ РЕАЛИЗОВАН (ORCH-115):** на `deploy-staging` для self-hosting `orchestrator` вердикт `staging_status:` производит детерминированный `src/staging_runner.py` (перехват в `launch_job` до `_spawn`, как D1/D2) — маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C (ORCH-036). LLM-ветвь остаётся fallback'ом под выключенным флагом / для не-self репо |
| **D1** | `src/agents/launcher.py:389` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `407`) | post-deploy edge | deploy-finalizer | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Занимает слот агента, но LLM не консультируется — рабочий прецедент замены |
| **D2** | `src/agents/launcher.py:394` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `428`) | post-deploy observation | post-deploy-monitor | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Тик наблюдения; LLM не консультируется |
> **Итог (поимённо).** `avoidable LLM control paths = {tester, deployer}`; control-path-но-keep =
> `{reviewer}`; не-control-path (P) = `{analyst, architect, developer}`; already-deterministic-эталон =
> `{deploy-finalizer, post-deploy-monitor}`.
### 1.1 Машинно-читаемый блок инвентаря
> Стабильный заголовок таблицы (`id | role | location | output_consumer | consults_llm | axis |
> avoidable | classification`) парсится `tests/test_llm_call_site_inventory.py` (split по `|`, без
> новых зависимостей) и сверяется с кодом (TC-03/04/05/13/14). **Не менять заголовок и значения без
> синхронной правки кода/тестов.**
<!-- ORCH-118-INVENTORY-BLOCK:START -->
| id | role | location | output_consumer | consults_llm | axis | avoidable | classification |
|----|------|----------|-----------------|--------------|------|-----------|----------------|
| S0 | _spawn | src/agents/launcher.py:472 | - | transport | - | - | - |
| A1 | analyst | .openclaw/agents/analyst.md | check_analysis_complete | yes | P | no | keep-LLM |
| A2 | architect | .openclaw/agents/architect.md | check_architecture_done | yes | P | no | keep-LLM |
| A3 | developer | .openclaw/agents/developer.md | check_ci_green | yes | P | no | keep-LLM |
| A4 | reviewer | .openclaw/agents/reviewer.md | check_reviewer_verdict | yes | C | no | keep-LLM |
| A5 | tester | .openclaw/agents/tester.md | _parse_tests_verdict | yes | C | yes | needs-hybrid-fallback |
| A6 | deployer | .openclaw/agents/deployer.md | _parse_staging_status | yes | C | yes | replace-deterministic-now |
| D1 | deploy-finalizer | src/agents/launcher.py:389 | - | no | - | - | already-deterministic |
| D2 | post-deploy-monitor | src/agents/launcher.py:394 | - | no | - | - | already-deterministic |
<!-- ORCH-118-INVENTORY-BLOCK:END -->
### 1.2 keep-LLM — названное суждение (обоснование)
> Для каждой `keep-LLM`-записи назван **конкретный** вид суждения, ради которого LLM сохраняется.
> Для C-keep (`reviewer`) обоснование явно фиксирует **НЕ-деривируемость** вердикта (почему не сводится
> к exit-коду). Парсится TC-05 (`- role: текст`).
<!-- ORCH-118-KEEP-JUSTIFICATION-BLOCK:START -->
- analyst: анализ требований и написание BRD/ТЗ — настоящее суждение; детерминированный гейт `check_analysis_complete` судит лишь наличие файлов, не их содержательное качество.
- architect: архитектурное решение и ADR — настоящее суждение о компромиссах/инвариантах; `check_architecture_done` судит лишь наличие 06-adr/07.
- developer: написание кода — настоящее суждение; гейт `check_ci_green` судит CI/merge, а не самоотчёт агента.
- reviewer: «приемлемость кода/решения» — настоящее суждение, которое НЕ деривируемо (not derivable) из exit-кода `pytest`/smoke/деплоя; в отличие от tester/deployer вердикт reviewer'а не сводится к tool-сигналу, поэтому это control-path-но-keep, а не avoidable.
<!-- ORCH-118-KEEP-JUSTIFICATION-BLOCK:END -->
---
## 2. Таксономия классификации (4 класса, выведена из осей)
Четыре взаимоисключающих класса; класс **выводится** из осей §0 (а не постулируется):
- **`keep-LLM`** — нужно настоящее суждение (обязательно **назвать** конкретное суждение, §1.2).
- **`replace-deterministic-now`** — безопасная детерминированная замена сейчас.
- **`replace-later/risky`** — замена возможна позже / с предпосылками.
- **`needs-hybrid-fallback`** — детерминированное ядро + LLM-фолбэк только на суждение.
**Правило вывода:**
`P → keep-LLM`; `C + не-деривируемый вердикт → keep-LLM`;
`C + деривируемый вердикт → replace-* / needs-hybrid-fallback` (**= avoidable**).
`deploy-finalizer`/`post-deploy-monitor` помечены `already-deterministic` — вне таксономии замен
(эталон: LLM не консультируется, перехват до `_spawn`).
---
## 3. Детерминизм не-агентских control-path'ов (доказательство, FR-3 / AC-3)
Эти пути **не консультируют LLM** (ни через `_spawn`-транспорт, ни альтернативным транспортом). ⚠️ Они
**спавнят subprocess'ы** (`git`/`pytest`/`docker`/`ssh`/сканеры/`staging_check.py`) — это
детерминированные **инструменты**, не LLM: доказательство детерминизма — **отсутствие *LLM*-транспорта**,
а не отсутствие *subprocess* (дискриминатор §0). Проверяется TC-02.
| Путь / модуль | `file:line` (якорь) | Природа |
|---------------|---------------------|---------|
| Маршрутизация стадий | `src/stages.py::STAGE_TRANSITIONS:12`, `advance_stage` в `src/stage_engine.py` | статический словарь + детерминированная функция |
| Реестр Quality Gate | `src/qg/checks.py::QG_CHECKS:812` (14 имён) | словарь имя→функция |
| Все `check_*` | `src/qg/checks.py` (`33/62/82/182/336/473/599/657`, …) | файловые/HTTP/exit-code проверки |
| Парсеры вердиктов `_parse_*` | `src/qg/checks.py:226/413/538` через `src/frontmatter.py::parse_frontmatter` | YAML-frontmatter парс (читают, **не производят** вердикт) |
| Классификатор ошибок | `src/error_classifier.py` | regex по строкам |
| Под-гейты | `src/{security_gate,merge_gate,coverage_gate,staging_verdict}.py` | сканеры/`pytest --cov`/git/маппинг exit-кодов |
| Self-deploy Phase A/B/C | `src/self_deploy.py` | детерминированный detached-деплой (ORCH-036) |
| Сериализация / владение | `src/{serial_gate,transition_lease,reconciler,job_reaper}.py` | FIFO-гейт / durable-lease / CAS / reaper |
Любая найденная **неожиданная** LLM-консультация в этих путях добавляется в инвентарь §1 и
классифицируется §2 (тогда TC-02 станет красным — точка дрейфа).
---
## 4. Скоуп и анти-дрейф
- **Docs + tests only (ORCH-118).** `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
`coverage_status:`) / схема БД — **байт-в-байт не тронуты** (это инвариант самой карты).
- **Реализованные срезы.** Первый срез roadmap'а**deployer (staging-status)** — реализован
**ORCH-115** (`src/staging_runner.py`, перехват в `launch_job` до `_spawn`): на `deploy-staging`
для in-scope репо вердикт `staging_status:` производит детерминированный код, не LLM. Это
`replace-deterministic-now` без ввода второго транспорта (раннер LLM не зовёт) — карта/инвариант
«единственный транспорт S0» соблюдены. Машинный `ORCH-118-INVENTORY-BLOCK` сохраняет deployer как
`avoidable=yes`/`axis=C` (LLM-ветвь жива как fallback под выключенным флагом / для не-self репо).
**Второй срез — tester (`result:`)** — реализован **ORCH-116** (`src/test_runner.py`, тем же
перехватом до `_spawn`): на `testing` для in-scope репо вердикт `result:` производит
детерминированный код (exit-код `pytest` в worktree + read-only smoke), не LLM. Это
`needs-hybrid-fallback`, тоже без второго транспорта (раннер LLM не зовёт). Машинный
`ORCH-118-INVENTORY-BLOCK` сохраняет tester как `avoidable=yes`/`axis=C`/
`classification=needs-hybrid-fallback` — LLM-ветвь жива как fallback (выключенный флаг / репо без
контракта) и как будущий **off-control-path** триаж, который не выносит `result:`.
- **Анти-дрейф тесты:** `tests/test_llm_call_site_inventory.py` (TC-01…TC-06, TC-09, TC-12, TC-13,
TC-14) и `tests/test_llm_determinization_docs.py` (TC-07/08/11). Дискриминатор всех проверок —
**«консультирует LLM», а не «спавнит subprocess»**.
- **Связанные документы:** [`llm-usage-policy.md`](llm-usage-policy.md),
[`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).

View File

@@ -0,0 +1,92 @@
# LLM determinization roadmap (ORCH-118)
> **Что это.** Упорядоченный план детерминированных замен **avoidable LLM control paths**
> (`{tester, deployer}` — см. [`llm-call-sites.md`](llm-call-sites.md)). Это **транзиентный план**:
> он обновляется по мере закрытия follow-up'ов. ORCH-118 раннеры **не реализует** (FR-7); старт каждого
> кандидата гейтится утверждением карты.
>
> **Кандидаты названы ПО РОЛИ.** Конкретные follow-up Plane-ID **не фиксируются** ни в одном артефакте
> (R3 / NFR-6): этих work item нет в подтверждённом backlog; ID присваивается при заведении задачи.
> Анти-фабрикация прибита тестом `TC-11` (`tests/test_llm_determinization_docs.py`).
>
> **Оценки экономии — «оценка до фактического замера» (NFR-5).** Источник — существующие колонки
> `agent_runs` (`model`/`effort`/токены/стоимость/время); точные числа снимаются при заведении
> follow-up'а через `GET /metrics` / запрос к `agent_runs`. Здесь — порядок величины, а не контракт.
---
## 1. Рекомендованный первый срез — **deployer (staging-status)** — ✅ РЕАЛИЗОВАН (ORCH-115)
> **Статус: реализовано.** Срез выполнен в **ORCH-115** — `src/staging_runner.py` (перехват в
> `launch_job` до `_spawn`, как `D1`/`D2`): на стадии `deploy-staging` для self-hosting `orchestrator`
> вердикт `staging_status:` производит детерминированный код (маппинг exit-кода `staging_check.py`
> через `self_deploy.map_exit_code_to_status`), а не LLM. Под kill-switch `staging_runner_enabled` +
> скоуп `staging_runner_repos` (пусто → self-hosting only); LLM-ветвь остаётся fallback'ом.
> Контракт артефакта/гейта `check_staging_status`/`STAGE_TRANSITIONS`/схема БД — не тронуты. Детали —
> `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`, сквозной
> `docs/architecture/adr/adr-0048-deterministic-staging-runner.md`. Запись `rank 1` в машинном блоке
> §4 сохраняется (`first_slice = yes`, инвариант карты) как историческая фиксация первого среза.
Обоснование (самый низкорисковый «чисто деривируемый» control path):
1. **Деривируемость максимальна.** Вердикт `staging_status:`**чистый маппинг** exit-кода
`scripts/staging_check.py`; готовый leaf `src/staging_verdict.py::compute_staging_verdict` (ORCH-061)
уже считает этот вердикт детерминированно.
2. **Прод уже детерминирован.** Ребро `deploy` (`deploy_status:`) для self-hosting `orchestrator`
производит детерминированный finalizer (Phase A/B/C, ORCH-036) — LLM в критическом
self-restart-пути нет. Срез не трогает критический путь → минимальная поверхность риска.
3. **Опирается на прецедент** `D1`/`D2` (`launch_job`-перехват **до** `_spawn`) — архитектурный риск
замены агента уже снят рабочим паттерном.
4. **`replace-deterministic-now`, без hybrid-fallback** (в отличие от tester).
## 2. Второй кандидат — **tester (гибрид)** — ✅ РЕАЛИЗОВАН (ORCH-116)
> **Статус: реализовано.** Срез выполнен в **ORCH-116** — `src/test_runner.py` (перехват в
> `launch_job` до `_spawn`, как `D1`/`D2`/ORCH-115): на стадии `testing` для self-hosting
> `orchestrator` вердикт `result:` производит детерминированный код (exit-код `pytest tests/` в
> worktree ветки + read-only smoke `/health`/`/status`/`/queue`+`serial_gate`, маппинг через
> `self_deploy.map_exit_code_to_status` в токенах `PASS`/`FAIL`), а не LLM. Под kill-switch
> `test_runner_enabled` + скоуп `test_runner_repos` (пусто → self-hosting only) + резолв тест-контракта
> (репо без контракта → LLM-tester, fail-safe). Контракт артефакта/гейта `check_tests_passed`/
> `STAGE_TRANSITIONS`/схема БД — не тронуты. Запись `rank 2` в машинном блоке §4 сохраняется
> (`first_slice = no`, `hybrid_needed = yes` — инвариант карты) как фиксация второго среза.
Детерминированное ядро (`pytest` + smoke даёт PASS/FAIL по exit-коду) покрывает основной вердикт;
LLM-фолбэк сохраняется **только** на суждение, не сводимое к exit-коду: **триаж падений** и **маппинг
TC ↔ критерии приёмки**. Поэтому `needs-hybrid-fallback`, а не `replace-deterministic-now`: поверхность
суждения шире и объём работы больше. В Phase 1 (ORCH-116) детерминированное ядро вынесено в раннер;
off-control-path LLM-триаж (он **не** выносит и **не** переопределяет `result:`, **не** добавляет ребро
в `STAGE_TRANSITIONS`) зафиксирован как Phase 2 follow-up по роли и в этом срезе не реализуется.
## 3. Общие требования к каждому follow-up'у
Каждый кандидат при заведении задачи несёт: kill-switch + обратимость (паттерн
ORCH-022/027/043/089/090 — флаг `*_enabled`, пустой CSV `*_repos` → self-hosting only), скоуп-гард
(не трогать `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схему БД), а замена-агента —
перехват в `launch_job` **до** `_spawn` (как `D1`/`D2`). Свежесть прецедента — инцидент-трек
ORCH-110/111/112/113/114/117 (единое детерминированное владение side-effectful путями).
---
## 4. Машинно-читаемый блок roadmap
> Заголовок (`rank | role | dependencies | savings_estimate_source | security_risk | hybrid_needed |
> followup_type | first_slice`) парсится `tests/test_llm_determinization_docs.py::test_tc07_*`.
> `followup_type` — **по роли**, без Plane-ID. Ровно один `first_slice = yes`.
<!-- ORCH-118-ROADMAP-BLOCK:START -->
| rank | role | dependencies | savings_estimate_source | security_risk | hybrid_needed | followup_type | first_slice |
|------|------|--------------|-------------------------|---------------|---------------|---------------|-------------|
| 1 | deployer | staging_verdict.compute_staging_verdict (ORCH-061) + launch_job pre-spawn precedent (D1/D2) | agent_runs (deployer rows; estimate pending GET /metrics) | low (prod already deterministic via Phase A/B/C ORCH-036) | no | deployer-replacement (staging-status mapping) | yes |
| 2 | tester | deterministic pytest+smoke core; LLM fallback for failure triage / TC-to-criteria mapping | agent_runs (tester rows; estimate pending GET /metrics) | medium (failure-triage judgment must stay correct) | yes | tester-hybrid (deterministic core + LLM fallback) | no |
<!-- ORCH-118-ROADMAP-BLOCK:END -->
---
## 5. Вне scope
- `reviewer` — C, но **keep** (вердикт «приемлемость кода/решения» не деривируем): **не** в roadmap'е.
- `analyst`/`architect`/`developer` — P (artifact-producer, не control path): **не** в roadmap'е.
- Реализация раннеров — отдельные follow-up задачи (по роли), стартуют после утверждения карты.
Связанные документы: [`llm-call-sites.md`](llm-call-sites.md), [`llm-usage-policy.md`](llm-usage-policy.md).

View File

@@ -0,0 +1,108 @@
# LLM usage policy (ORCH-118)
> **Нормативный durable-документ.** Формулирует принцип использования LLM в оркестраторе, критерии
> «keep vs replace» через **control-path-ось**, и нормативное **определение «avoidable LLM control
> path»**. Применяется ко **всем** будущим правкам control-path'ов. Сопутствующие артефакты —
> карта [`llm-call-sites.md`](llm-call-sites.md) и roadmap [`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).
---
## 1. Принцип
**LLM — только там, где нужно настоящее суждение.** Если решение/вердикт control-path'а есть
**детерминированная функция tool-сигналов**, которые оркестратор уже вычисляет (exit-code `pytest`,
smoke, `staging_check.py`, статус деплоя, наличие файлов, CI-статус), — оно должно приниматься
**детерминированно**, а не консультацией LLM. LLM сохраняется там, где требуется суждение, **не
сводимое** к tool-сигналу (анализ требований, архитектурное решение, написание кода, приемлемость
ревью).
Это защищает **автономность** (NFR-2): меньше точек, где недетерминизм/стоимость/латентность LLM
встроены в поток управления, и меньше класса инцидентов «LLM-агент принял решение, которое на деле
есть исполнение фиксированных команд и маппинг результата» (RCA-трек ORCH-110/111/112/113/114/117).
---
## 2. Три оси решения (ground-truth — код)
1. **consultation ≠ transport/slot.** «LLM консультируется» ⇔ решение/артефакт конвейера **потребляет
суждение LLM**. Существование транспорта (`_spawn`) или слота агента (job-роли с перехватом до
`_spawn`) — это **capability**, не консультация.
2. **control-path (C) ≠ artifact-producer (P)** — определяется **кодом-потребителем** вывода роли:
- **(C)** LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт** → суждение входит в
поток управления.
- **(P)** LLM производит артефакт, а продвижение решает **детерминированный гейт** независимо
(наличие файлов / CI) → суждение в control flow не входит.
3. **деривируемость вердикта** — вердикт C-консультации либо детерминированная функция tool-сигналов,
либо настоящее суждение, не сводимое к exit-коду.
---
## 3. Нормативное определение «avoidable LLM control path»
Это **двухбитный проверяемый предикат над `src/qg/checks.py`**, а не «удобство на глаз».
<!-- ORCH-118-AVOIDABLE-DEFINITION-BLOCK:START -->
Call-site является **avoidable LLM control path** тогда и только тогда, когда выполнены **оба** условия:
- **(i)** это **C (control-path)** консультация — её LLM-вердикт потребляется потоком управления
(`check_*`-гейт ветвится на нём: PASS → дальше / FAIL → откат);
- **(ii)** вердикт **деривируем** (derivable) из tool-сигналов, которые оркестратор уже вычисляет сам —
exit-code `pytest` / smoke / `staging_check.py` / статус деплоя.
Если оба условия выполнены, суждение LLM не добавляет информации → консультацию можно снять без потери
смысла (заменить детерминированным раннером или гибридом с LLM-фолбэком только на не-деривируемую часть).
<!-- ORCH-118-AVOIDABLE-DEFINITION-BLOCK:END -->
**Поимённый целевой набор** (сверен с кодом, прибит тестами TC-13/TC-14):
- **avoidable LLM control paths = `{tester, deployer}`** — C **и** вердикт деривируем
(`result:` = exit-code `pytest`+smoke; `staging_status:` = маппинг exit-кода `staging_check.py`).
- **`reviewer`** — C, но **keep**: вердикт «приемлемость кода/решения» **НЕ деривируем** из exit-кода
(настоящее суждение). Это control-path-но-keep, **не** avoidable.
- **`analyst` / `architect` / `developer`** — **не** control path (**P**, artifact-producer):
детерминированный гейт судит артефакт независимо.
---
## 4. Критерии решения: keep vs replace
| Ситуация (по осям §2) | Решение | Класс |
|-----------------------|---------|-------|
| **P** — artifact-producer (детерминированный гейт судит артефакт) | **keep** LLM | `keep-LLM` |
| **C**, вердикт **НЕ деривируем** (настоящее суждение) | **keep** LLM (назвать суждение) | `keep-LLM` |
| **C**, вердикт **деривируем**, замена безопасна сейчас | **replace** | `replace-deterministic-now` |
| **C**, вердикт деривируем, но замена позже / с предпосылками | **replace later** | `replace-later/risky` |
| **C**, ядро деривируемо, но часть требует суждения | **hybrid** (детерм. ядро + LLM-фолбэк) | `needs-hybrid-fallback` |
> **keep-LLM требует обоснования:** любая `keep-LLM`-запись обязана **назвать конкретное суждение**;
> для C-keep — явно зафиксировать **не-деривируемость** вердикта (почему не сводится к exit-коду).
---
## 5. Требование к новым/изменённым control-path'ам (норматив)
- **Обоснование против политики.** Любой **новый** или изменённый control-path, который консультирует
LLM, обязан в своём ADR обосновать это против настоящей политики: показать, что он **P** (artifact
judged independently) **или** **C с не-деривируемым** вердиктом. C-консультация с деривируемым
вердиктом — это `avoidable`; её ввод без обоснования reviewer ловит как finding ≥P1.
- **Reviewer-ось (как ORCH-079) — требование, не реализация гейта.** Политика **рекомендует**
reviewer'у проверять соответствие новых control-path'ов настоящей политике; ORCH-118 **не** вводит
новый Quality Gate (`QG_CHECKS`/`check_*` не меняются) — это нормативное требование процесса.
- **Норматив сопровождения.** Меняешь место вызова LLM или потребителя вердикта в `src/qg/checks.py`
обнови карту [`llm-call-sites.md`](llm-call-sites.md) и эту политику **в том же PR** (анти-дрейф
держат TC-13/TC-14).
- **Единственный транспорт.** Единственный разрешённый транспорт LLM-консультации в `src/**` — это
`launcher._spawn` (S0). Ввод второго транспорта (новый `_spawn`, импорт `anthropic`/`openai`/иного
LLM-SDK, прямой HTTP Anthropic/Claude, второй model-invoking subprocess) запрещён без явного ADR;
прибито тестами TC-01/TC-12.
- **Реализованный срез (ORCH-115).** Снятие C-консультации с деривируемым вердиктом — это разрешённое
`replace-deterministic-now`, а не ввод новой LLM-консультации. ORCH-115 снял A6/staging-status:
детерминированный `src/staging_runner.py` производит `staging_status:` без `_spawn` (перехват до
него, как `D1`/`D2`) — раннер **LLM не зовёт** и **второй транспорт не вводит**, поэтому инвариант
«единственный транспорт S0» соблюдён (TC-12 зелёный). Это образец для последующих срезов roadmap'а.
- **Реализованный срез (ORCH-116).** ORCH-116 снял A5/tester тем же паттерном: детерминированный
`src/test_runner.py` производит `result:` на `testing` для in-scope репо без `_spawn` (перехват до
него, как `D1`/`D2`) — exit-код `pytest` в worktree + read-only smoke. Это `needs-hybrid-fallback`
(детерминированное ядро вынесено; LLM-фолбэк на не-деривируемое суждение — триаж падений / маппинг
TC↔критерии — остаётся **off-control-path** и **не** производит `result:`). Раннер **LLM не зовёт** и
**второй транспорт не вводит** → инвариант «единственный транспорт S0» соблюдён (TC-12 зелёный).
Будущий off-control-path триаж — **не** новый транспорт control-path-консультации (он вне control-path).

View File

@@ -179,8 +179,8 @@ docker compose -f deploy/bundled/docker-compose.yml ps
Доводка «одним запуском»: preflight → секреты → up/готовность → init Gitea
(полностью автоматом: админ-бот + API-токен) → init Plane → онбординг
sandbox-проекта **строго** кирпичом `onboard_project.py` (22 канонических
статуса, включая fail-closed **`Confirm Deploy`** и **`STOP`**, лейблы,
sandbox-проекта **строго** кирпичом `onboard_project.py` (23 канонических
статуса, включая fail-closed **`Confirm Deploy`**, **`STOP`** и **`Оценка`**, лейблы,
репо+webhook — golden source `docs/operations/ONBOARDING.md` §1) → git-доступ
агентов → сборка `.env`/`.env.watchdog` → health.

View File

@@ -206,12 +206,13 @@ curl -fsS "$ORCH_PLANE_API_URL/api/v1/workspaces/<workspace-slug>/projects/" \
`ORCH_PLANE_API_TOKEN` в `.env`. Токен должен иметь право создавать проекты/статусы
(нужно для `onboard_project.py apply`, §10).
**5.3. Модель статусов — НЕ вручную.** Конвейеру нужны **22 канонических статуса** с
**5.3. Модель статусов — НЕ вручную.** Конвейеру нужны **23 канонических статуса** с
точными именами и группами; их создаёт `python3 scripts/onboard_project.py apply` (§10),
полная таблица — `docs/operations/ONBOARDING.md` §1 (golden source; здесь не дублируется).
Два имени фиксируем явно, потому что они **fail-closed** (без них ветка просто не
активируется, без ошибки): **`Confirm Deploy`** (человеческий гейт прод-деплоя) и
**`STOP`** (отмена задачи; обязан быть в группе `cancelled`).
Три имени фиксируем явно, потому что они **fail-closed** (без них ветка просто не
активируется, без ошибки): **`Confirm Deploy`** (человеческий гейт прод-деплоя),
**`STOP`** (отмена задачи; обязан быть в группе `cancelled`) и **`Оценка`** (триггер
оценки задачи, ORCH-020; группа `unstarted`/`backlog`, НЕ терминальная).
```bash
# после §10 — проверить, что статусы созданы:
@@ -219,7 +220,7 @@ curl -fsS "$ORCH_PLANE_API_URL/api/v1/workspaces/<workspace-slug>/projects/<proj
-H "X-API-Key: $ORCH_PLANE_API_TOKEN" | python3 -m json.tool | grep -c '"name"'
```
**Проверка:** счётчик имён = 22 (или больше, если в проекте остались дефолтные статусы
**Проверка:** счётчик имён = 23 (или больше, если в проекте остались дефолтные статусы
Plane) и среди них `Confirm Deploy` и `STOP` — PASS.
**5.4. Webhook + HMAC.** Приёмник — `POST https://<orchestrator-public-host>/webhook/plane`;
@@ -432,7 +433,7 @@ curl -fsS http://127.0.0.1:8501/health
## 10. Регистрация проекта заказчика
Onboarding-CLI создаёт Plane-проект с 22 статусами и лейблами (`autoApprove` /
Onboarding-CLI создаёт Plane-проект с 23 статусами и лейблами (`autoApprove` /
`autoDeploy` / `Bug`), Gitea-репо с webhook'ом, скелет репо (kit) и печатает merged-реестр.
Полный runbook — `docs/operations/ONBOARDING.md`; Lite-последовательность:

View File

@@ -0,0 +1,87 @@
# FAQ: отмена задачи через статус STOP
Эта страница — для пользователя доски Plane. Она объясняет простыми словами, что делает статус
**STOP**, как им безопасно остановить задачу и чего от него ждать. Читать код для этого не нужно.
Технические детали механизма — в
[инженерном обзоре конвейера](../overview/tech-pipeline.md#отмена-stop--cancelled) и в
[ADR ORCH-090](../work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md) (источник истины
поведения). Эта страница их **не дублирует**, а пересказывает «для человека» и ссылается на них.
## Что делает статус STOP?
STOP — это «кнопка отмены» задачи. Перевод задачи в статус STOP останавливает работу агента,
снимает задачу с очереди, прибирает рабочие материалы (ветку и worktree) и помечает задачу
отменённой (`cancelled`). Нажимать его безопасно даже посреди конвейера.
## Как отменить задачу?
Переведите issue в статус **STOP** на доске Plane — так же, как меняете любой другой статус.
Предусловие: на доске должен быть заведён статус **STOP** (группа `cancelled`). Если его нет, STOP
не сработает (см. раздел «Я нажал STOP, но ничего не произошло — почему?»).
## Что происходит, когда я нажимаю STOP?
По шагам:
1. Активный агент **останавливается** (мягкая остановка процесса).
2. Все **job'ы** этой задачи в очереди снимаются и больше не перезапускаются.
3. Рабочая **ветка** задачи и её **worktree** удаляются. Ветка `main` и прод-контейнер при этом
никогда не трогаются.
4. Задача переходит в терминальное состояние **`cancelled`**.
5. Приходит уведомление в **Telegram** («🛑 … задача ОТМЕНЕНА (STOP)») и **комментарий в Plane**.
При этом **документы задачи** (бизнес-запрос, анализ, ТЗ, ADR и т.д.) **сохраняются** — удаляются
только рабочая ветка и worktree, не история документов.
## Что если задача в этот момент сливается или деплоится?
Если STOP пришёл во время **необратимого шага** (слияние в `main` или выкладка в прод), отмена
**аккуратно откладывается** до честного завершения этого шага. Вы увидите уведомление вида
«⏸️ … отмена ОТЛОЖЕНА». Ветка `main` и прод при этом не трогаются; как только шаг честно
завершится, отмена применяется автоматически.
Иными словами: STOP **не прерывает** уже начатый необратимый шаг на полпути — он дожидается его
честного завершения, а затем отменяет задачу.
## Откатит ли STOP уже выложенный код?
**Нет.** STOP сбрасывает **незавершённый прогресс** задачи (рабочую ветку, worktree, очередь), но
**не откатывает** код, который уже влит в `main` или выложен в прод. Откат уже выложенного —
это отдельная задача (revert), и STOP её **не делает**.
## Как перезапустить отменённую задачу?
Отменённую задачу **нельзя «продолжить с середины»**. Чтобы начать заново, переведите её в статус
**«To Analyse»** — задача будет создана **с нуля**: новая ветка от актуального `main`, новый анализ
и полный проход конвейера.
## Я нажал STOP, но ничего не произошло — почему?
Вероятные причины:
- На доске **нет статуса STOP** — переход не распознаётся (безопасный no-op). Заведите статус STOP
(группа `cancelled`) и попробуйте снова.
- Задача **уже завершена или уже отменена** — повторный STOP ничего не меняет (это нормально,
идемпотентный no-op, задача не ломается).
- Отмена **отключена для репозитория** настройкой оператора (`stop_status_enabled` /
`stop_status_repos`) — обратитесь к оператору.
## Где увидеть, что задача отменена?
- **Карточка задачи в Telegram** покажет «🛑 … ОТМЕНЕНА (STOP)».
- В **Plane** появится комментарий об отмене.
- Оператор может увидеть отмену на служебной странице состояния `GET /queue` — в блоке `stop`.
## STOP, Approved и Confirm Deploy — в чём разница?
Это разные управляющие статусы, их легко перепутать:
- **STOP** — *отменить* задачу и сбросить её незавершённый прогресс.
- **Approved** — *одобрить* артефакт анализа (двигает задачу дальше по конвейеру); деплой он **не**
запускает.
- **Confirm Deploy** — *подтвердить* выкладку в прод.
Подробнее об управляющих статусах и их семантике — в
[инженерном обзоре конвейера](../overview/tech-pipeline.md). Эта страница описывает только STOP.

View File

@@ -21,6 +21,14 @@
/repos/<project> ← общий каталог репозиториев (host: /home/slin/repos)
```
> **Инвариант deploy-базы (ORCH-112, нормативно).** Shared main checkout
> `<host_repos_dir>/<repo>` (= `/home/slin/repos/orchestrator` == `/repos/orchestrator` в контейнере
> через bind-mount == `settings.deploy_host_repo_path`) — это **deploy/worktree-management база, НЕ
> редактируемый workspace.** Рабочие изменения туда **не пишутся** конвейером/агентами: агенты —
> worktree `/repos/_wt/<repo>/<branch>` (`git_worktree`), `docker build` — worktree-контекст,
> fallback'и гейтов — read-only `git show origin/main`. Self-deploy `git pull` устойчив к грязной
> базе (resilient-pull, см. self-hosting-страховки ниже).
## Контейнеры
| Контейнер | Роль | Порт | env_file | БД (хост) | Старт |
@@ -133,6 +141,8 @@ watchdog'а: **watchdog сигналит, pruner убирает**.
| `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_PLANE_TEST_WRITE_ENABLED` | ORCH-117: opt-in реальной записи в Plane из **тест-процесса** (дефолт `false` = default-deny). НЕ kill-switch прод-блока: даже `true` пишет только в sandbox-allowlist (прод-запись из pytest невозможна). В боевом/staging рантайме гард — no-op |
| `ORCH_PLANE_TEST_SANDBOX_PROJECTS` | ORCH-117: CSV-allowlist sandbox-проектов, куда opt-in разрешает запись из тестов (дефолт = единственный SANDBOX `8c5a3025-…`; пусто → ни один проект из тестов не пишется) |
| `ORCH_GITEA_URL` / `_TOKEN` / `_WEBHOOK_SECRET` | доступ к Gitea + HMAC |
| `ORCH_CLAUDE_BIN` | путь к claude CLI |
| `ORCH_REPOS_DIR` / `ORCH_HOST_REPOS_DIR` | каталог репозиториев (в контейнере / на хосте) |
@@ -216,15 +226,57 @@ watchdog'а: **watchdog сигналит, pruner убирает**.
**Что изолировано (безопасно):**
- Staging (8501) — отдельная БД (`./data/staging`), отдельный реестр (`ORCH_PROJECTS_JSON` = только sandbox). Прод-проекты не видит.
- Репозитории разделены, изоляция веток через git worktree (ORCH-2).
- **Запись в Plane из тест-процесса — sandbox-only fail-closed (ORCH-117).** Тест/worktree-процесс
наследует живой боевой Plane-токен (`PLANE_HEADERS`/`PROJECT_ID` захвачены на импорте `plane_sync`);
раньше **ничто** не мешало pytest смутировать боевую доску (инцидент ORCH-114 — «ложный Done»).
Теперь leaf `src/plane_write_guard.py` врезан в 3 примитива записи `plane_sync`
(`update_issue_state`/`add_comment`/`_set_issue_state_direct`) и **в тест-процессе** (детект
`pytest`-в-процессе) блокирует запись по умолчанию; разрешена только при opt-in
`ORCH_PLANE_TEST_WRITE_ENABLED=true` **И** целевом проекте ∈ `ORCH_PLANE_TEST_SANDBOX_PROJECTS`
(sandbox-only — боевой проект запрещён даже при opt-in). Боевой и staging рантаймы
(`uvicorn src.main:app`, без pytest в процессе) — гард **no-op**, запись как прежде. Прод-блок
**без kill-switch** (выключателя-чёрного-хода нет); второй слой — autouse-floor
`tests/conftest.py::_plane_sandbox_only` (по образцу `_no_telegram`). Детали — `CLAUDE.md`
«Sandbox-only fail-closed изоляция записи в Plane (ORCH-117)», adr-0046.
**Страховки:**
- Стадия `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.
- **Гигиена shared deploy-базы (ORCH-112):** self-deploy `git pull origin main` устойчив к грязному рабочему дереву deploy-базы (модифицированные tracked + untracked-остатки failed/cancelled/брошенных задач). Хук `--deploy` перед pull приводит базу к чистому `origin/main` (resilient-pull: `git fetch` + `git reset --hard origin/main` + `git clean -fd`), **строго сохраняя** rollback-снимки `.deploy-prev-image-*`, `deploy-hook.log`, gitignored `.env`/`data/`/`*.db` (НИКОГДА `-x`!), sibling `.deploy-state-*`/`.merge-lease-*.json`, `.git/worktrees/*`. Гейт — kill-switch `ORCH_CHECKOUT_HYGIENE_ENABLED` (дефолт `True`; off → голый pull 1:1); скоуп `ORCH_CHECKOUT_HYGIENE_REPOS` (пусто → self-hosting only). Грязь базы детектируется → лог + Telegram-алерт (Phase-C finalizer). Решает инцидент ORCH-111 (грязь ORCH-104 заблокировала `git pull`). Детально — `docs/work-items/ORCH-112/06-adr/ADR-001`, сквозной adr-0044.
### Граница исполнения docker-операций — host-side, НЕ изнутри прод-контейнера (ORCH-123)
**Инвариант (нормативно, adr-0049):** прод-контейнер `orchestrator` несёт **только**
`openssh-client git curl` (+ pinned gitleaks) — **бинаря `docker` в образе НЕТ** (`Dockerfile:11`,
`python:3.12-slim` его не несёт). `/var/run/docker.sock` **смонтирован** (`docker-compose.yml`,
rw + `group_add 999`, «МИНА 1» ORCH-040), но **сознательно НЕ используется изнутри** контейнера: нет
docker-клиента, и добавлять его (CLI/SDK) **нельзя** — это активировало бы дремлющий
root-эквивалентный путь для всего, что бежит в контейнере, включая LLM-агентов (security-разбор —
ADR-001 D2 / adr-0049 / R-1).
Поэтому **все** docker-операции self-hosting исполняются **host-side** через **ssh на `127.0.0.1`**
(`ORCH_DEPLOY_SSH_HOST`/`ORCH_DEPLOY_SSH_USER`, ssh-ключ смонтирован `:ro`), где docker CLI есть:
- **прод-деплой** (ORCH-036, `self_deploy.build_deploy_command`) — `ssh … setsid bash hook --deploy`;
- **image-freshness** (ORCH-058, `image_freshness.image_revision`/`rebuild_staging_image`) — `ssh … docker …`;
- **staging-runner** (ORCH-123, `staging_runner.build_staging_command`) — `ssh … docker exec orchestrator-staging python3 … staging_check.py … --mode stub`.
Сама staging-сюита (`scripts/staging_check.py`) **по-прежнему** исполняется **внутри** контейнера
`orchestrator-staging` (8501) — меняется лишь **кто инициирует** `docker exec` (хост через ssh, а не
прод-контейнер). До ORCH-123 staging-runner (ORCH-115) вызывал `docker exec` **изнутри**
прод-контейнера → `FileNotFoundError` (нет `docker`) → постоянный environment-дефект ложно
маршрутизировался как код-фейл-откат `deploy-staging → development` (инцидент ORCH-116). Фикс:
host-side ssh-обёртка (флаг `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=true`, дефолт) + трёхсторонняя
классификация (suite-ran / permanent-env / transient-infra), где environment/инфра **никогда** не
оканчивается код-фейл-откатом (infra-HOLD + алерт «инфра, НЕ дефект кода»), и prod-like preflight
канала на старте сервиса. Откат — `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` (прежний in-container
вызов — валиден лишь там, где docker CLI запечён в образ) или `ORCH_STAGING_RUNNER_ENABLED=false`
(LLM-deployer 1:1). Детали — `docs/work-items/ORCH-123/06-adr/ADR-001`, сквозной adr-0049.
**Правила для агентов при задачах ORCH:**
1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи.
2. Все проверки деплоя — на staging (8501), боевой 8500 не трогать.
3. Деплой self — только через хук с health-check + авто-rollback (`DEPLOY_HOOK.md`).
4. docker-операции исполняются **host-side через ssh** — в контейнере `docker` CLI нет (ORCH-123).
## Эксплуатация (быстрые команды)
```bash

View File

@@ -46,7 +46,7 @@ hooks под выбранным owner (`--gitea-owner`, дефолт из кон
1. **Проект**: создаётся с `identifier = --prefix`. Уже существует → передай
`--plane-project-id <uuid>` (ensure распознает и пропустит).
2. **Статусы — точные канонические имена** (22, источник — `plane_sync._PLANE_NAME_TO_KEY`;
2. **Статусы — точные канонические имена** (23, источник — `plane_sync._PLANE_NAME_TO_KEY`;
опечатка = тихая деградация fail-closed веток):
| Статус | Группа | | Статус | Группа |
@@ -62,9 +62,11 @@ hooks под выбранным owner (`--gitea-owner`, дефолт из кон
| Review | `started` | | **STOP** | **`cancelled`** |
| Testing | `started` | | Awaiting Deploy | `started` |
| Deploying | `started` | | Monitoring after Deploy | `started` |
| **Оценка** | **`unstarted`** | | | |
⚠️ Код-критично: `STOP` обязан быть в группе `cancelled` (иначе ветка отмены молча не
активируется); в терминальных группах (`completed`/`cancelled`) — ТОЛЬКО
активируется); **`Оценка`** (триггер оценки задачи, ORCH-020) — группа `unstarted`/`backlog`,
**НЕ** терминальная; в терминальных группах (`completed`/`cancelled`) — ТОЛЬКО
Done/Cancelled/STOP, иначе terminal-detection ложно сочтёт живую задачу терминальной.
3. **Лейблы**: `autoApprove`, `autoDeploy`, `Bug` (имена — из конфига оркестратора; их
отсутствие = fail-safe ручной режим / полный цикл).
@@ -149,7 +151,7 @@ hooks под выбранным owner (`--gitea-owner`, дефолт из кон
--webhook-url https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea
```
Проверяет: запись реестра парсится и совпадает по полям; все 22 статуса резолвятся
Проверяет: запись реестра парсится и совпадает по полям; все 23 статуса резолвятся
(включая fail-closed `Confirm Deploy`/`STOP`); лейблы на месте; webhook существует и
активен; kit-файлы в репо (6 промптов, `AGENTS.md`, `INFRA.md`, `_templates`/`_standards`);
нет неразрешённых плейсхолдеров. Любой gap → exit `2` с перечнем.

View File

@@ -42,6 +42,10 @@
аналитики и отдельной стадии проектирования, но через все те же гейты качества.
- **Отмена задачи одной кнопкой.** Перевод задачи в статус «STOP» в трекере останавливает
работу, снимает её с очереди и прибирает за собой — безопасно даже посреди конвейера.
- **Оценка задачи до запуска.** Перевод задачи в статус «Оценка» в трекере даёт прогноз её
стоимости, времени и сложности (story points) по истории похожих задач — можно оценить
сразу пачку задач и спланировать, что и когда брать в работу. По завершении задачи рядом
с прогнозом ложится факт — оценки калибруются на реальных данных.
- **Наблюдаемость.** У каждой задачи — живая карточка в Telegram (стадия, время, стоимость);
у платформы — служебные страницы состояния и машинные метрики; история отклонений пишется
в журнал уроков.
@@ -97,6 +101,13 @@
Передумали — переводите задачу в статус «STOP»: работа агента останавливается, ветка и
рабочие материалы прибираются, задача помечается отменённой. Если задача в этот момент в
необратимой фазе выкладки — отмена аккуратно откладывается до её честного завершения.
Подробнее — [FAQ по статусу STOP](../operations/FAQ_STOP.md).
### Сценарий 7: оценить бэклог перед планированием
Оператор выделяет несколько задач в бэклоге и переводит их в статус «Оценка». По каждой
платформа считает прогноз стоимости, времени и сложности по истории завершённых задач и
возвращает задачу в бэклог с проставленной оценкой и комментарием. Видно, что дорого, а что
дёшево — и чем наполнить ближайший прогон. По завершении задачи рядом ляжет факт.
---

View File

@@ -71,7 +71,7 @@
- Запуск: перевод задачи в статус «To Analyse» — единственная точка входа в конвейер
- Статусы Plane — индикация, а не управление: платформа выставляет их сама (Backlog → … → Done)
- Управляющих статусов ровно три: запуск, человеческие гейты и отмена
- Статусы-действия, на которые платформа реагирует: запуск, человеческие гейты, отмена (STOP) и оценка задачи («Оценка»)
- Ход задачи виден сразу: статусы доски, живая карточка в Telegram, комментарии в задаче со ссылками на ветку и PR
> Визуал: доска Plane с движущейся карточкой и зеркальное уведомление в Telegram
@@ -83,6 +83,7 @@
- Лейблы «autoApprove» / «autoDeploy» снимают эти два решения для пакетного авто-режима
- Авто-режим убирает только ожидание человека — ни одна техническая проверка не пропускается
- Лейбл «Bug» — короткий багфикс-маршрут; статус «STOP» — безопасная отмена с уборкой ветки и worktree, не трогает прод
- Статус «Оценка» — прогноз стоимости, времени и сложности задачи (можно пачкой) с возвратом в бэклог, без запуска LLM
> Визуал: две кнопки человека, переключатели авто-лейблов и стоп-кран STOP
@@ -137,6 +138,7 @@
- Пакет задач на ночь: метки авто-одобрения, утром всё на проде
- Багфикс по короткому маршруту с обязательным регресс-тестом
- Остановить задачу: статус STOP — безопасная отмена с уборкой
- Оценить бэклог: пачка задач в статус «Оценка» — прогноз и возврат в бэклог
- Несколько проектов параллельно без пересечений
> Визуал: пять пиктограмм-сценариев

View File

@@ -8,7 +8,7 @@
| Роль | Стадия | Вход | Выходные артефакты | Machine-verdict ключ |
|------|--------|------|--------------------|----------------------|
| `analyst` | analysis | бизнес-запрос (`00-business-request.md`) | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml` | — (гейт проверяет полноту пакета + одобрение человека) |
| `analyst` | analysis | бизнес-запрос (`00-business-request.md`) | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`; when-applicable сигнальный `01-questions.md` | — (гейт проверяет полноту пакета + одобрение человека) |
| `architect` | architecture | пакет аналитики | `06-adr/ADR-NNN-*.md`, when-applicable `07-infra-requirements.md` / `08-data-requirements.md`, `10-tech-risks.md` | — (гейт проверяет наличие ADR) |
| `developer` | development | ТЗ + ADR | код в `src/`, тесты в `tests/`, обновлённые доки, `CHANGELOG.md`, PR в Gitea | — (гейт — зелёный CI ветки) |
| `reviewer` | review | PR diff + ТЗ/ADR | `12-review.md` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) |
@@ -18,6 +18,15 @@
Machine-verdict ключи читаются гейтами **только из YAML-frontmatter** артефакта (никогда из
прозы) и неизменны байт-в-байт — подробнее в [блоке качества](tech-quality-security.md).
> **Сигнальный канал аналитика → Needs Input (ORCH-120).** Если на стадии `analysis` аналитик
> упирается в блокирующие открытые вопросы, он не фабрикует обязательные deliverables, а выпускает
> when-applicable артефакт `01-questions.md` — задача уходит в **Needs Input** и (под флагом
> `analyst_needs_input_autopause_enabled`, скоуп self-hosting) автоматически встаёт на паузу, чтобы
> не клинить очередь репозитория, пока ждём ответа человека; ответ возобновляет анализ и снимает
> паузу. `01-questions.md` — сигнальный артефакт того же владельца/стадии, **не** machine-verdict и
> **не** один из 4 обязательных deliverables (exit-гейт `check_analysis_complete` его не парсит). Как
> это вплетено в serial-gate — [конвейер](tech-pipeline.md).
## Модель и эффорт
Модель и эффорт каждой роли резолвятся **только из конфига** (не из промпта); текущие
@@ -48,6 +57,20 @@ Machine-verdict ключи читаются гейтами **только из Y
Особенность: промпт `deployer` сознательно на английском (самый safety-critical — несёт
запреты self-hosting в видной рамке); остальные пять — на русском.
Особенность (ORCH-115): на стадии `deploy-staging` для self-hosting `orchestrator` LLM-`deployer`
заменён **детерминированным staging-раннером** (`src/staging_runner.py`) — его работа была чисто
механической (запуск staging-сюиты + маппинг exit-кода). LLM-промпт `deployer` остаётся fallback'ом
под выключенным флагом / для не-self репо и продолжает вести прод-стадию `deploy`. Подробнее —
[конвейер](tech-pipeline.md) и [карта LLM-консультаций](../architecture/llm-call-sites.md).
Особенность (ORCH-116): на стадии `testing` для self-hosting `orchestrator` LLM-`tester` заменён
**детерминированным test-раннером** (`src/test_runner.py`) — его PASS/FAIL-ядро деривируемо (exit-код
`pytest` в worktree + read-only smoke), вердикт `result:` производит детерминированный код. Это
гибрид (`needs-hybrid-fallback`): LLM-промпт `tester` остаётся fallback'ом под выключенным флагом / для
репо без тест-контракта, а будущий off-control-path триаж падений не выносит и не переопределяет
`result:`. Подробнее — [конвейер](tech-pipeline.md) и
[карта LLM-консультаций](../architecture/llm-call-sites.md).
## Человек как седьмая роль
Человек не пишет артефакты конвейера, но принимает два решения, которые не делегированы

View File

@@ -32,6 +32,7 @@ worker запустил агента стадии → результат про
| **Очередь задач** (`jobs` + worker) | Собственная очередь на SQLite: атомарный захват job'а, ретраи с backoff, зависимости между job'ами, ограничение параллелизма. |
| **State machine** (`src/stages.py`) | Карта стадий `STAGE_TRANSITIONS`: для каждой стадии — следующая, агент и гейт выхода. Единственный источник истины о конвейере. |
| **Stage engine** (`src/stage_engine.py`) | Исполняет переходы: диспетчеризация гейтов, откаты, под-гейты деплойного ребра, синхронизация статусов с Plane. |
| **Transition-lease** (`src/transition_lease.py`) | Durable-владение side-effectful переходом стадии: один владелец на задачу (lease на входе + expected-stage CAS на записи), liveness по pid+boot-id. Не даёт конкурентному или после-рестартовому повторному входу дважды применить необратимый эффект (merge / деплой / ratchet). |
| **Agent launcher** (`src/agents/launcher.py`) | Запускает Claude CLI агента в изолированном git worktree ветки задачи, следит за процессом (watchdog), авто-продвигает стадию по завершении. |
| **Реестр гейтов** (`src/qg/checks.py`) | `QG_CHECKS` — машинные проверки выхода со стадий; вердикты читаются только из YAML-frontmatter артефактов. |
| **Plane-sync** (`src/plane_sync.py`) | Индикация статусов в Plane (слой «показать человеку», никогда не управление конвейером). |

View File

@@ -19,7 +19,8 @@ Project ──1:N──► Work-Item / Task ──1:N──► Job ──1:N─
### Work-Item / Task — задача конвейера
Строка таблицы `tasks`: текущая **стадия** (`stage`), **маршрут** (`track`: полный или
багфикс), рабочая **ветка**, счётчики откатов, отметки отмены. Натуральные ключи — ID задачи
багфикс), рабочая **ветка**, счётчики откатов, отметки отмены и **паузы** (`paused_at`
durable-сигнал «пропустить меня в serial gate», не терминальный). Натуральные ключи — ID задачи
в Plane и человекочитаемый номер (`ORCH-NNN`). На каждой стадии задача накапливает
**артефакты** — номерные документы в `docs/work-items/<ID>/` (от бизнес-запроса до
deploy-лога; манифест — [PIPELINE_DOCS](../_standards/PIPELINE_DOCS.md)).
@@ -47,6 +48,8 @@ deploy-лога; манифест — [PIPELINE_DOCS](../_standards/PIPELINE_DOC
| `coverage_baseline` | базовая линия покрытия тестами; растёт только вверх (ratchet) |
| `tracker_messages` | леджер всех Telegram-карточек задачи (зачистка сирот) |
| `lessons` | машинный журнал уроков — структурированные отклонения конвейера |
| `transition_lease` | durable-владение side-effectful переходом стадии: один владелец на задачу, liveness по pid+boot-id (предотвращает двойное применение необратимых эффектов) |
| `task_estimates` | леджер прогноз↔факт оценки задачи (стоимость/время/токены/story points), ключ `work_item_id` — фундамент калибровки оценок |
Все изменения схемы — аддитивные и идемпотентные (`CREATE TABLE IF NOT EXISTS`, ensure-column
при старте): обновление платформы не требует ручных миграций.

View File

@@ -8,13 +8,15 @@
- **Вход конвейера:** перевод задачи в статус «To Analyse» — единственная точка запуска
пайплайна. Вебхуки Plane (HMAC-подписанные) доставляют изменения задач.
- **Статусы = индикация, не управление** ([блок 2](tech-pipeline.md)): платформа сама
выставляет статусы доски, чтобы человек видел осмысленную картину; управляют конвейером
только машина стадий и три управляющих статуса (запуск, человеческие гейты, STOP).
выставляет статусы доски, чтобы человек видел осмысленную картину; саму машину стадий не
подменяет ни один статус. Платформа реагирует лишь на **операторские статусы-действия**:
запуск, человеческие гейты (Approved/Confirm Deploy), STOP (отмена) и «Оценка» (прогноз
задачи с возвратом в Backlog).
- **Лейблы** — декларативные переключатели на задаче: `autoApprove` / `autoDeploy` (снятие
человеческих гейтов), `Bug` (багфикс-маршрут). Источник истины — Plane API: ошибка чтения
лейблов трактуется как «лейбла нет» (fail-safe — никогда не «авто» по ошибке).
- Платформа пишет в задачу комментарии о ходе работ (под ботами ролей) с кликабельными
ссылками на ветку/PR.
ссылками на ветку/PR; для оценённых задач — прогноз (в поле оценки) и факт по завершении.
## Gitea — git, PR, CI

View File

@@ -12,6 +12,7 @@
- стоимость задачи нарастающим итогом (токены/доллары по каждому запуску агента);
- честные метрики времени на финише: время агентов / время ожидания человека / общее
календарное — три независимые цифры, а не одна вводящая в заблуждение сумма;
- пункт «Оценка» (прогноз стоимости · времени · токенов), если задача оценивалась;
- кликабельный номер задачи (ведёт в Plane), отметка багфикс-маршрута.
Карточка тихая (без пингов); пингуют только алерты: красный гейт, ожидание решения человека,
@@ -20,8 +21,13 @@
## Служебные страницы платформы
- **`GET /queue`** — человекочитаемый снимок всего конвейера: очередь и job'ы, состояние
serial gate и заморозок, авто-лейблы, багфикс-трек, coverage, журнал уроков, фоновые
демоны. Первая точка диагностики «что сейчас происходит».
serial gate (заморозки, паузы задач, причина ожидания успешника), авто-лейблы, багфикс-трек,
coverage, журнал уроков, владение переходами (`transition_lease`), оценки задач (`estimator`),
фоновые демоны. Первая
точка диагностики «что сейчас происходит». Паузу/возобновление задачи в serial gate включают
два источника: **оператор** — явными эндпоинтами `POST /serial-gate/pause|resume`, и **движок**
автоматически, когда аналитик задаёт блокирующие вопросы и задача уходит в Needs Input (авто-park;
снимается на возобновлении; под флагом `analyst_needs_input_autopause_enabled`, скоуп self-hosting).
- **`GET /metrics`** — машинный контракт для внешнего наблюдателя (версионированная схема):
health, возраст последних событий, счётчики сбоев.
- **`GET /health`** — живость процесса.

View File

@@ -34,6 +34,27 @@ created → analysis → architecture → development → review → testing →
| `done` | — | — | терминал |
| `cancelled` | — | — | терминал (сток отмены) |
> **Детерминированный staging-раннер (ORCH-115).** На стадии `deploy-staging` для self-hosting
> `orchestrator` работу ведёт **детерминированный код** (`src/staging_runner.py`), а не LLM-агент
> `deployer`: он перехватывается в `launch_job` до запуска агента (как Phase A/B/C прод-деплоя),
> исполняет ту же staging-сюиту, маппит exit-код в `staging_status:` и инициирует **тот же** гейт
> `check_staging_status`. Это замена *продюсера* артефакта, а не гейта: контракт `15-staging-log.md`,
> имя/семантика `check_staging_status`, `STAGE_TRANSITIONS` — не изменились. Под kill-switch
> `staging_runner_enabled` (скоуп `staging_runner_repos`, пусто → self-hosting only); при выключении
> на стадии снова работает LLM-`deployer` байт-в-байт. Это первый реализованный срез
> determinization-roadmap (см. `docs/architecture/llm-determinization-roadmap.md`).
> **Детерминированный test-раннер (ORCH-116).** На стадии `testing` для self-hosting `orchestrator`
> работу ведёт **детерминированный код** (`src/test_runner.py`), а не LLM-агент `tester`: он
> перехватывается в `launch_job` до запуска агента (тем же паттерном, что staging-раннер), исполняет
> регресс `pytest` в worktree ветки + read-only smoke, маппит exit-код в `result:` и инициирует **тот
> же** гейт `check_tests_passed`. Это замена *продюсера* артефакта, а не гейта: контракт
> `13-test-report.md`, имя/семантика `check_tests_passed`/`_parse_tests_verdict`, `STAGE_TRANSITIONS`
> — не изменились. Под kill-switch `test_runner_enabled` (скоуп `test_runner_repos`, пусто →
> self-hosting only; репо без тест-контракта → LLM-tester); при выключении снова работает LLM-`tester`
> байт-в-байт. Это второй реализованный срез determinization-roadmap (гибрид: LLM-фолбэк остаётся на
> off-control-path триаж, не на вынесение `result:`).
## Под-гейты деплойного ребра — врезки, не стадии
На переходе `deploy-staging → deploy` исполняются четыре под-гейта в нормативном порядке
@@ -86,6 +107,18 @@ created → analysis → architecture → development → review → testing →
прода после выкладки замораживает репозиторий (freeze) до ручного разбора — следующие задачи
ждут.
У FIFO-порядка есть управляемое исключение — **пауза без блокировки**: более раннюю задачу можно
снять с активной очереди репозитория, не дожидаясь её завершения, и тогда срочный успешник её
обгоняет. Паузу (durable-сигнал `tasks.paused_at`) ставят два источника. **Оператор** — явно
(`POST /serial-gate/pause`, снять — `/resume`). **Движок** — автоматически, когда аналитик
упирается в блокирующие открытые вопросы и задача уходит в **Needs Input** (узкий триггер под
флагом `analyst_needs_input_autopause_enabled`, скоуп self-hosting); на возобновлении (ответ
человека) движок снимает паузу симметрично. Авто-park нужен, чтобы задача, ждущая человека часы
или дни, не клинила FIFO-очередь репозитория в автономном пакетном прогоне. Пауза — отдельная ось:
она ≠ отмена (задача не терминальна и возвращается в гейт обратной командой) и **не** обходит ни
freeze, ни объявленные зависимости. Свежесть базы возобновлённой задачи гарантируют те же
механизмы (отложенный срез ветки + ребейз на слиянии), что и для обычного FIFO.
## Отмена: STOP → `cancelled`
Перевод задачи в статус **STOP** останавливает агента, снимает job'ы с очереди, удаляет
@@ -93,12 +126,27 @@ created → analysis → architecture → development → review → testing →
(идёт слияние/выкладка) — отмена откладывается и применяется после честного завершения шага.
STOP никогда не трогает `main` и прод-контейнер.
Пользовательская инструкция «как этим пользоваться» — [FAQ по статусу STOP](../operations/FAQ_STOP.md).
## Оценка задачи: статус «Оценка»
Перевод backlog-задачи в статус **«Оценка»** (в т.ч. **массово** — multi-select Plane)
запускает прогноз её стоимости, времени, токенов и сложности (story points `1/2/3/5/8`) по
истории похожих завершённых задач — детерминированной эвристикой, **без запуска LLM**. Прогноз
пишется в поле оценки задачи, публикуется комментарием и пунктом «Оценка» в Telegram-карточке,
после чего задача **возвращается в Backlog**. Это операторский side-жест: он **не двигает задачу
по конвейеру** (не ребро `STAGE_TRANSITIONS`), не занимает слот очереди и не выдёргивает
in-flight работу. Пере-оценка — повторный перевод в «Оценка» (идемпотентно). По завершении самой
задачи рядом с прогнозом сохраняется **факт** — фундамент калибровки оценок.
## Статусная модель Plane: индикация ≠ управление
Статусы в Plane — слой **индикации**: они показывают человеку осмысленную картину хода задачи,
но никогда не управляют конвейером (машина стадий — только `STAGE_TRANSITIONS`). Управляющих
статусов ровно три: запуск в работу, Approved/Confirm Deploy (человеческие гейты) и STOP
(отмена). Полная карта статусов — в [инженерном справочнике](../architecture/README.md).
но никогда не управляют конвейером (машина стадий — только `STAGE_TRANSITIONS`). Отдельно стоят
**операторские статусы-действия**, на которые платформа реагирует: запуск в работу,
Approved/Confirm Deploy (человеческие гейты), STOP (отмена) и «Оценка» (прогноз с возвратом в
Backlog, см. выше). Они инициируют действие или снимают ожидание человека, но саму машину стадий
не подменяют. Полная карта статусов — в [инженерном справочнике](../architecture/README.md).
---

View File

@@ -40,6 +40,28 @@
- Анти-регресс машинный: структурные тесты сканируют исполняемый код на боевые хост-литералы,
а документацию — на секретоподобные значения; находка рвёт CI.
## Где уместен LLM: карта вызовов и нормативная политика
Платформа держит **доказательную карту** всех мест, где поток управления потребляет суждение
LLM, и **нормативную политику** «LLM — только там, где нужно настоящее суждение». Карта разводит
три факта: консультация ≠ транспорт/слот; **control-path** (вердикт LLM ветвит поток управления)
**artifact-producer** (детерминированный гейт судит артефакт независимо); и деривируемость
вердикта из tool-сигналов. Путь называется **avoidable LLM control path**, когда он одновременно
control-path и его вердикт деривируем из exit-кодов (тогда консультацию можно заменить
детерминированным раннером). Поимённо: avoidable = `{tester, deployer}`; настоящее суждение
сохраняется у `{analyst, architect, developer, reviewer}`. Карта — снимок, прибитый структурными
анти-дрейф тестами. **Первый срез реализован (ORCH-115):** на `deploy-staging` для self-hosting
`orchestrator` LLM-`deployer` заменён детерминированным `src/staging_runner.py` (вердикт
`staging_status:` = маппинг exit-кода staging-сюиты); LLM-ветвь остаётся fallback'ом, гейт
`check_staging_status` не тронут. **Второй срез реализован (ORCH-116):** на `testing` для self-hosting
`orchestrator` LLM-`tester` заменён детерминированным `src/test_runner.py` (вердикт `result:` = exit-код
`pytest` + read-only smoke); это гибрид (`needs-hybrid-fallback`) — LLM-ветвь остаётся fallback'ом /
будущим off-control-path триажем, гейт `check_tests_passed`/`_parse_tests_verdict` не тронут.
- Карта вызовов LLM: [`../architecture/llm-call-sites.md`](../architecture/llm-call-sites.md)
- Нормативная политика: [`../architecture/llm-usage-policy.md`](../architecture/llm-usage-policy.md)
- Порядок замен: [`../architecture/llm-determinization-roadmap.md`](../architecture/llm-determinization-roadmap.md)
## Self-hosting-страховки
Платформа дорабатывает сама себя тем же конвейером — прод-инстанс при этом обслуживает и

View File

@@ -0,0 +1,7 @@
# Business Request: Оценка задачи: стоимость, время и сложность (адаптивный выбор моделей)
Work Item ID: ORCH-020
## Description
Оценка задачи: стоимость, время и сложность (для адаптивного выбора моделей)Цель: добавить оркестратору функцию ОЦЕНКИ задачи — прогноз стоимости и времени реализации. Шаг 2: оценка СЛОЖНОСТИ задачи для адаптивного выбора моделей агентов.📊 Шаг 1 — Оценка стоимости и времениПеред запуском (или на этапе аналитики) оркестратор прогнозирует: сколько будет стоить задача (токены × тариф = $) и сколько займёт времени.Данные уже есть: учёт токенов и cost_usd по прошлым задачам (наблюдаемость из PR #20). ET-014 ~35 мин — есть фактура для калибровки.База оценки — история похожих задач (по типу/стадиям/стеку): средние токены/время/стоимость.Где показывать: коммент в Plane / Telegram-уведомление при заведении задачи — Слава видит прогноз до старта.Post-factum: сравнение прогноз vs факт → уточнение модели оценки (петля, связь с ORCH-8 саморазвитие).🧠 Шаг 2 — Оценка сложности → адаптивный выбор моделейКлассификация сложности задачи (trivial / small / medium / complex) на этапе триажа/аналитики.На основе сложности — адаптивно выбирать модель агента: простая задача → дешёвая/быстрая модель; сложная → сильная модель. Прямая связь с ORCH-13 (мультипровайдерность).Оптимизация: не жечь дорогую модель на тривиальной правке (экономия), но не недокормить сложную задачу слабой моделью (качество).Сложность также влияет на выбор трека (связь с ORCH-19 багфикс: trivial → hotfix, complex → полный цикл).🔧 Что проработатьСигналы для оценки сложности: текст постановки, тип (фича/баг), затронутые файлы/стек, исторические аналоги.Кто оценивает: отдельный шаг-оценщик, под-функция аналитика, или эвристика на входе.Модель оценки: эвристика по истории / отдельный LLM-вызов-оценщик / гибрид.Точность vs стоимость самой оценки (оценка не должна стоить дороже экономии).Маппинг сложность → модель (конфигурируемый, связь с ORCH-13 и манифестом проекта ORCH-9).🔗 СвязкиORCH-13 (мультипровайдерность) — оценка сложности кормит адаптивный выбор модели. Тесная пара.ORCH-19 (багфикс-трек) — сложность определяет глубину пайплайна.ORCH-8 (саморазвитие) — петля прогноз vs факт уточняет модель оценки.PR #20 (наблюдаемость, токены/cost_usd) — фактура для калибровки.❓ Открытые вопросы СлавеГде показывать прогноз стоимости/времени — Telegram при заведении, Plane-коммент, оба?Оценка обязательна для каждой задачи или по запросу/для крупных?Адаптивный выбор модели — автоматический по сложности, или с твоим подтверждением для дорогих?Шкала сложности — фиксированная (trivial/small/medium/complex) или числовая (story points)?Создано 2026-06-04. Статус: Backlog, на проработку. Источник: голосовая постановка Славы + раскладка Стрим.

View File

@@ -0,0 +1,238 @@
---
work_item: ORCH-020
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 01 — BRD (бизнес-требования): ORCH-020 — Оценка задачи (прогноз стоимости/времени/story points), запускаемая статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
> **Revision после REJECT (Plane, 2026-06-17).** Заказчик отклонил предыдущий пакет: «**что я не
> увидел в БРД — как запускать оценку?** Я хотел бы переводить задачу в статус "Оценка", после чего
> запускался бы механизм оценки, и после завершения оценки задача бы меняла статус на backlog. На
> оценку я буду отправлять задачи **массово через Plane**. Также я могу **переоценивать задачи много
> раз**.» Этот раунд **переписывает модель триггера**: оценка теперь — **операторское действие,
> запускаемое выделенным Plane-статусом «Оценка»** (а не «автоматически для каждой задачи на
> `start_pipeline`», как в отклонённой версии). Прочие требования (что прогнозируем, куда пишем,
> леджер прогноз↔факт, leaf-инварианты) сохранены и согласованы с новым триггером. Полный пакет
> `01``04` supersedeит прежний по mtime.
## 1. Бизнес-контекст и проблема
Заказчик планирует работу по бэклогу вручную и хочет **до отправки задачи в работу** видеть прогноз:
сколько задача будет стоить (токены × тариф = $), сколько займёт времени и насколько она сложна
(размер в story points). Сейчас этих данных до старта нет: оркестратор собирает фактуру
(`input_tokens`/`output_tokens`/`cache_*`/`cost_usd`/`model`/`effort`, тайминги
`agent_runs.started_at/finished_at`, `tasks.created_at/updated_at`) **только постфактум** через
`src/usage.py` (`task_usage_summary`, `agent_cost_totals`, `record_usage`). Контур **прогноза до
старта** отсутствует.
**Корень REJECT — отсутствовал способ ЗАПУСКА оценки.** Заказчик мыслит оценку как **операторский
жест в Plane**, а не как невидимый авто-шаг: он сам решает, какие задачи бэклога оценить, **массово**
переводит их в выделенный статус, получает прогнозы и продолжает планирование. Отклонённая версия
прятала триггер в `start_pipeline` («оценка обязательна для каждой задачи автоматически») и явно
называла точку триггера «реализационной деталью» — это и есть то, что заказчик «не увидел» и
отверг.
Цитаты заказчика (Plane, 2026-06-17):
- REJECT: «как запускать оценку? Я хотел бы **переводить задачу в статус "Оценка"**, после чего
запускался бы механизм оценки, и после завершения оценки задача бы **меняла статус на backlog**. На
оценку я буду отправлять задачи **массово через Plane**. Также я могу **переоценивать задачи много
раз**.»
- Раунд Needs Input: «В Plane есть поле оценка, туда и нужно записывать оценку. По факту завершения
задачи вписать в смежное поле… для оценки есть два поля.»; «Только Шаг 1, без выбора модели»;
«Модели не выбираем и не меняем. Это вне скоупа».
Установленные факты по коду (на которые опирается решение, не изобретать):
- **Прецедент «статус-триггер уже есть в платформе.** Plane-статусы — слой B (индикация, ORCH-066) и
НЕ управляют машиной стадий; но платформа уже имеет **операторские action-статусы**, запускающие
side-механизмы: **STOP** (ORCH-090, отмена задачи) и **Confirm Deploy** (ORCH-059, прод-деплой).
Оба разбираются в `webhooks/plane.py::handle_issue_updated` через
`proj_states.get("<key>")` и оба **намеренно отсутствуют** в `plane_sync._DEFAULT_STATES`
(fail-closed: доска без статуса → `None` → ветка не активируется). Статус «Оценка» — **третий
представитель этого же семейства**.
- **Маппинг имени статуса → логический ключ** — `plane_sync._PLANE_NAME_TO_KEY` (`"STOP"→"stop"`,
`"Confirm Deploy"→"confirm_deploy"`); `get_project_states` резолвит UUID статуса per-project из
Plane API.
- **Массовость — «бесплатно».** Plane multi-select по N задачам в статус «Оценка» порождает N
отдельных `issue.updated`-вебхуков (по одному на issue); каждый обрабатывается независимо. Отдельный
«batch-UX» в оркестраторе не требуется — массовость обеспечивает сам Plane.
- **Фактура для калибровки уже накоплена** (`agent_runs`, агрегаты `task_usage_summary` /
`agent_cost_totals`, тайминги). Это сырьё для «истории похожих задач».
- **Plane-поля существуют.** На issue присутствуют поля `estimate_point` (ОЦЕНКА) и `point` (ФАКТ);
estimate-система на проекте (`project.estimate`) на момент анализа **не настроена** — инфра-
предусловие (NFR-7).
- **Выбор модели/эффорта статичен по роли** (`resolve_agent_model`/`resolve_agent_effort`,
ORCH-41/74; дефолт `claude-opus-4-8`) и в этой задаче **не трогается** (Шаг 2 вне объёма).
- **leaf-паттерн платформы** (`serial_gate`/`coverage_gate`/`labels`/`lessons`/`cancel`): never-raise,
kill-switch `*_enabled`, `*_repos` CSV (пусто → self-hosting only), read-only блок в `GET /queue`.
## 2. Объём (scope)
### В объёме (Шаг 1 — Оценка, запускаемая статусом)
- **Триггер «Оценка» (ядро правки).** Перевод issue в выделенный Plane-статус **«Оценка»** запускает
механизм оценки этой задачи. Оператор делает это **вручную и массово** (multi-select в Plane).
- **Жизненный цикл статуса:** `Backlog → (оператор) «Оценка» → [оркестратор: оценка] → (оркестратор)
Backlog`. По завершении оценки оркестратор **сам возвращает** issue в статус **`Backlog`**.
- **Пере-оценка много раз.** Повторный перевод в «Оценка» переоценивает задачу заново (идемпотентно:
перезапись `estimate_point` и строки леджера). Применимо при изменении скоупа.
- **Прогноз четырёх величин:** стоимость ($), время, токены и **сложность в story points** из
фиксированной шкалы `{1, 2, 3, 5, 8}`.
- **Шкала story points (фиксированная, ответ Q-3 = вариант A):** `1` — мелкая docs/label/config;
`2` — небольшой фикс; `3` — средняя; `5` — сложная (код + тесты); `8` — эпик / разбивать.
- **Запись прогноза в Plane-поле `estimate_point`** (это ОЦЕНКА).
- **Запись факта в Plane-поле `point`** по завершении задачи (фактическая реализованная сложность в
story points из фактических токенов/времени/стоимости по той же шкале) — для калибровки.
- **Отображение прогноза на двух поверхностях** (ответ Q-5 = оба): Plane-коммент + пункт **«Оценка»**
в общей Telegram-карточке задачи (`src/notifications.py`) — **время, токены, стоимость**.
- **Локальный леджер прогноз↔факт** (фундамент петли калибровки, связь с ORCH-8): хранение прогноза,
факта и дельты, **ключ — `work_item_id`** (issue может ещё не иметь pipeline-задачи на момент
оценки — она на бэклоге).
### Вне объёма
- **Шаг 2 — адаптивный выбор моделей агентов** (ответы Q-1/Q-2: «Только Шаг 1, без выбора модели»;
«Модели не выбираем и не меняем. Это вне скоупа»). Горячий путь `resolve_agent_model`/
`resolve_agent_effort`/`_spawn` **не модифицируется**.
> **ACTION (поручение заказчика, Plane 16:34):** «заведи отдельную задачу в Plane для адаптивного
> выбора модели и укажи зависимость на мультипровайдеров (ORCH-13)». Создание Plane-issue — действие
> уровня заказчика/PM и **вне write-объёма аналитика** (Write ограничен `docs/work-items/<id>/*`).
> Фиксирую как обязательный follow-up: новый work item «Адаптивный выбор модели агента по сложности»
> с зависимостью на **ORCH-13**; оценщик сложности из ORCH-020 — его вход. Оператору: подтвердить
> создание или создать вручную.
- **Автопереключение трека по сложности** (связка с ORCH-19) — позже; здесь сложность лишь
вычисляется и публикуется как сигнал.
- **Авто-ретроспективщик / RICE-приоритизатор** (E2/E3 ORCH-8) — вне объёма; леджер — фундамент.
- **Автоматическая оценка КАЖДОЙ задачи на `start_pipeline`** — **исключена явно** (модель
отклонённой версии). Оценка — операторский on-demand жест через статус «Оценка».
- **Изменение тарифной/биллинговой модели** — используется существующий `cost_usd` из `usage.py`.
- **Новый «batch-UX»/массовый эндпоинт как ОСНОВНОЙ путь** — не нужен (массовость даёт Plane
multi-select → N вебхуков). Программный `POST /estimate*` допустим лишь как **опциональное**
удобство/диагностика, не как основной триггер (см. TRZ §4).
## 3. Заинтересованные стороны
- **Заказчик / владелец продукта (Слава)** — инициатор оценки (переводит задачи в «Оценка»),
потребитель прогноза для планирования бэклога; принимает результат.
- **Оркестратор (self-hosting)** — носитель функции; общий прод обслуживает и `enduro-trails`.
- **Будущая петля саморазвития (ORCH-8)** — потребитель леджера прогноз↔факт для калибровки.
- **ORCH-13 (мультипровайдерность)** — будущий потребитель сигнала сложности (через follow-up Шаг 2).
## 4. Бизнес-требования (BR)
### Триггер и жизненный цикл (ядро ревизии)
- **BR-T1 — Запуск оценки статусом «Оценка».** Перевод issue в выделенный Plane-статус **«Оценка»**
запускает оценку именно этой задачи. Это **единственный обязательный** способ запуска (массовый и
ручной), реализуемый по образцу операторских action-статусов STOP (ORCH-090) / Confirm Deploy
(ORCH-059).
- **BR-T2 — Авто-возврат в Backlog.** По завершении оценки (успех или best-effort-пропуск)
оркестратор **сам** переводит issue обратно в статус **`Backlog`**. Заказчик видит задачу
вернувшейся в бэклог с заполненным `estimate_point`.
- **BR-T3 — Массовость через Plane.** Массовый перевод N задач в «Оценка» (multi-select Plane)
оценивает все N; каждый issue обрабатывается независимо (N вебхуков). Отдельный массовый UX в
оркестраторе не требуется.
- **BR-T4 — Пере-оценка много раз (идемпотентно).** Повторный перевод задачи в «Оценка»
переоценивает её заново; прогноз и строка леджера **перезаписываются** (не дублируются). Число
пере-оценок не ограничено.
- **BR-T5 — Fail-closed статус.** На доске без статуса «Оценка» (enduro / частичная конфигурация /
Plane недоступен) триггер **не активируется** (ключ резолвится в `None`) — нулевая регрессия;
это инфра-предусловие (NFR-7), а не ошибка.
- **BR-T6 — Не нарушать машину стадий и in-flight работу.** Статус «Оценка» запускает **side-
механизм**, а не переход стадии. Если у issue есть **активная** pipeline-задача (queued/running
job), триггер — **no-op + лог** (не выдёргивать выполняемую работу в Backlog, не трогать
`STAGE_TRANSITIONS`). Авто-возврат в Backlog **не** создаёт цикла: статус `Backlog` ни одной веткой
`handle_issue_updated` не обрабатывается (no-op-эхо).
### Содержание оценки (сохранено, согласовано с триггером)
- **BR-1 — Прогноз.** Для задачи оркестратор производит прогноз четырёх величин: **стоимость ($)**,
**время**, **токены** и **сложность в story points** из фиксированной шкалы `{1,2,3,5,8}`.
- **BR-2 — База оценки — история.** Прогноз строится на истории похожих **завершённых** задач (по
типу/стадиям/стеку): средние токены/время/стоимость из уже накопленной фактуры (`agent_runs`,
`task_usage_summary`, `agent_cost_totals`, тайминги). При отсутствии истории — разумный bootstrap-
дефолт (не блокирует).
- **BR-3 — Шкала story points фиксированная** с точной семантикой `1/2/3/5/8` (см. §2). Значение `8` —
«эпик: разбивать».
- **BR-4 — On-demand, не блокирующая.** Оценка производится **по запросу** (перевод в «Оценка»), а не
для каждой задачи автоматически; строго best-effort — сбой/выключение оценки **никогда** не тормозит
конвейер и не меняет маршрут.
- **BR-5 — Доступность до старта работы.** Поскольку оператор оценивает задачи на **бэклоге** (до
`To Analyse`/`start_pipeline`), прогноз доступен **до** перевода задачи в работу — он и нужен для
планирования отправки задач.
- **BR-7 — Запись прогноза в Plane.** Прогноз сложности (story points) записывается в поле issue
**`estimate_point`** (= ОЦЕНКА).
- **BR-8 — Запись факта в Plane.** По завершении задачи (переход в `done`) фактическая реализованная
сложность (story points из фактических токенов/времени/стоимости по той же шкале) записывается в
смежное поле **`point`** — для калибровки; прогноз `estimate_point` при этом **не перезаписывается**.
- **BR-9 — Отображение на двух поверхностях.** Прогноз публикуется: (a) **Plane-комментом**;
(b) пунктом **«Оценка»** в общей Telegram-карточке задачи — **время, токены, стоимость**.
- **BR-10 — Леджер прогноз↔факт (калибровка).** Прогноз и факт сохраняются локально вместе с дельтой
(ключ `work_item_id`); фундамент петли уточнения модели оценки (связь с ORCH-8). Достаточно
фиксировать обе величины и дельту (авто-уточнение модели — позже).
## 5. Нефункциональные требования (NFR)
- **NFR-1 — Оценка ≠ Quality Gate / ≠ переход стадии.** Модуль — наблюдатель/продюсер.
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи (`verdict:`/`result:`/
`deploy_status:`/`staging_status:`/`security_status:`/`coverage_status:`) / схемы **существующих**
таблиц — **байт-в-байт не тронуты**. Статус «Оценка» не добавляет ребра в машину стадий; он
запускает side-механизм и сам возвращает issue в Backlog.
- **NFR-2 — leaf-паттерн.** never-raise (любой сбой → warning + безопасный дефолт), kill-switch
`*_enabled`, скоуп `*_repos` (CSV; **пусто → self-hosting only**), read-only блок в `GET /queue`.
- **NFR-3 — self-hosting safety.** Модуль не рестартит/не роняет прод-контейнер, не трогает `main`/
force-push, **не вмешивается в горячий путь запуска агентов** (`resolve_agent_model`/
`resolve_agent_effort`/`_spawn` не модифицируются). Выключенный флаг / неприменимый репо → нулевая
регрессия для `enduro-trails` и `orchestrator`.
- **NFR-4 — Стоимость оценки ≪ её ценности.** Сама оценка должна быть дешёвой и быстрой относительно
выгоды планирования. Выбор механизма (эвристика по истории / отдельный LLM-вызов / гибрид) и баланс
«точность vs стоимость» — **архитектурное** решение (`06-adr`); в TRZ — лишь требование-ограничение.
- **NFR-5 — Толерантность к массовости.** Массовый перевод (десятки задач разом → десятки вебхуков
почти одновременно) **не должен** перегружать прод/конвейер: оценка best-effort, изолирована от
control-path; механизм сглаживания нагрузки (дешёвая эвристика / очередь / троттлинг) — деталь
`06-adr`. Требование: bulk не роняет и не тормозит обслуживание других проектов.
- **NFR-6 — Запись в Plane через существующие примитивы.** `estimate_point`/`point`/коммент/возврат в
Backlog пишутся через `plane_sync` и подчиняются sandbox write-guard (ORCH-117): в боевом рантайме
(`uvicorn`) — штатная запись, из тест/worktree-процесса — заблокирована. **Новых секретов/токенов не
вводится.**
- **NFR-7 — Fail-safe и инфра-предусловия Plane.** (a) Статус **«Оценка»** должен существовать на
доске проекта (его отсутствие = fail-closed no-op, BR-T5). (b) estimate-система Plane со значениями
`1/2/3/5/8` (Fibonacci) для `estimate_point` должна быть настроена; при её отсутствии запись
`estimate_point`/`point` **best-effort пропускается** (+ лог) и **не роняет** конвейер. Детали и
точные группы статуса — `07-infra-requirements.md` (архитектор).
- **NFR-8 — Обратная совместимость данных.** Хранение прогноз↔факт — **аддитивная** новая таблица
(`CREATE TABLE IF NOT EXISTS`); существующие таблицы/колонки не изменяются.
## 6. Допущения и ограничения
- Оценка на бэклоге работает по **issue** (описание/тип/лейблы из Plane API + история похожих), а не
по локальной pipeline-задаче: на момент оценки `tasks`-строки может **не быть** → леджер и запись
ключуются по `work_item_id`, `task_id` — нуллабелен до старта пайплайна.
- Статус «Оценка» — транзиентный (issue в нём лишь на время оценки, затем Backlog); его Plane-группа
(`backlog`/`unstarted`) косметична — деталь онбординга/инфры (ORCH-009 расширяется на 23-й статус).
- Фактура `usage.py`/`agent_runs` достаточна для расчёта факта при завершении; «фактические story
points» выводятся из факта по той же шкале `{1,2,3,5,8}`.
- Без ORCH-13 «выбор модели» бессмыслен (один дефолт) — Шаг 2 корректно вынесен в follow-up.
- Точная Plane-семантика `estimate_point` (FK на estimate-point estimate-системы) vs `point`
(целочисленный) — деталь реализации/инфры (архитектор + NFR-7).
## 7. Критерии успеха
Заказчик **массово переводит** задачи бэклога в статус **«Оценка»**; по каждой оркестратор
производит прогноз (стоимость/время/токены/story points), пишет его в `estimate_point`, публикует в
Plane-комменте и пункте «Оценка» Telegram-карточки, сохраняет в леджер прогноз↔факт и **возвращает
issue в Backlog**; пере-оценка повтором перевода идемпотентна; по завершении задачи факт пишется в
`point`. Всё это — без единого изменения control-path/гейтов, без касания горячего пути запуска
агентов, без выдёргивания in-flight работы; на доске без статуса «Оценка» / при выключенном флаге —
нулевая регрессия. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
## 8. Риски
- **Статус «Оценка» дёргает in-flight задачу** → снимается BR-T6 (no-op при активном job) + авто-
возврат только в Backlog, никогда не трогая стадии.
- **Цикл вебхуков** (возврат в Backlog → новый webhook) → снимается тем, что `Backlog` не
обрабатывается ни одной веткой `handle_issue_updated` (no-op-эхо) — анти-loop по построению.
- **Перегрузка от массового перевода** → снимается NFR-5 (best-effort, дешёвый механизм/сглаживание —
`06-adr`).
- **Запись в боевой Plane** (`estimate_point`/`point`/коммент/состояние) на общей доске → снимается
write-guard (ORCH-117) + best-effort/fail-safe (NFR-6/NFR-7).
- **Неточность прогноза на холодном старте** (мало истории) → bootstrap-дефолт + петля калибровки
(BR-10).
- **Расползание в Шаг 2** (control-path) → жёсткий out-of-scope + NFR-3.
Детальный разбор — `10-tech-risks.md` (архитектор).

View File

@@ -0,0 +1,132 @@
---
work_item: ORCH-020
stage: analysis
author_agent: analyst
status: needs-input
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 01 — Открытые вопросы (Open Questions): ORCH-020 — Оценка задачи: стоимость, время и сложность (адаптивный выбор моделей)
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
> **Сигнальный** when-applicable артефакт (ORCH-120, adr-0053). Пишется аналитиком ТОЛЬКО при
> **блокирующей** неоднозначности, когда выпустить корректные 4 deliverables (`01-brd`/`02-trz`/
> `03-acceptance-criteria`/`04-test-plan`) нельзя без ответа заказчика. **Не** machine-verdict;
> гейтом не парсится — сигнал движку (`_handle_analysis_approved_flow`) увести задачу в **Needs
> Input**. После ответов в Plane аналитик перезапускается (resume), читает свежие комментарии и
> выпускает полный пакет.
## 1. Контекст
Бизнес-запрос (`00-business-request.md`) ставит **две связанные, но разные по риску** функции и
**сам перечисляет 4 нерешённых вопроса для Славы** («❓ Открытые вопросы Славе»). Это решения
уровня владельца продукта, а не аналитические дефолты: они определяют **объём**, **модель данных**
и — главное — затрагивается ли **прод-control-path выбора модели агента** на self-hosting инстансе,
который из ОДНОГО процесса с ОБЩЕЙ БД обслуживает и `enduro-trails`.
**Что установлено по фактическому коду (карта src/, на которую опираются вопросы):**
- **Фактура для оценки уже есть, но только post-run.** `src/usage.py` парсит токены/`cost_usd`/
модель из вывода Claude CLI ПОСЛЕ финиша агента; `agent_runs` несёт `input_tokens`/`output_tokens`/
`cache_*`/`cost_usd`/`model`/`effort`; есть агрегаты `task_usage_summary`/`agent_cost_totals` и
тайминги (`agent_runs.started_at/finished_at`, `tasks.created_at/updated_at/brd_review_*`).
**Прогноз ДО старта** как контур сейчас отсутствует — это новый продюсер, не правка гейта.
- **Выбор модели/эффорта статичен по роли.** `resolve_agent_model`/`resolve_agent_effort`
(`src/agents/launcher.py`, ORCH-41/74) резолвят модель из config/project-override и применяют её в
`_spawn` (`--model`/`--effort`, launch-стамп ORCH-109). **Хука «варьировать модель по сложности
задачи нет».** Любой адаптивный per-task override — это вмешательство в горячий путь запуска
агентов на общем проде.
- **ORCH-13 (мультипровайдерность) ещё не реализован.** Дефолт один — `claude-opus-4-8`; реально
различимы лишь **тиры эффорта** (`low/medium/high/xhigh/max`). Без ORCH-13 «выбор модели»
фактически вырождается в «выбор эффорта».
- **`tasks.track` (ORCH-019) уже существует** (`'full'|'bug'`) — связка «сложность → трек»
опирается на готовый механизм, отдельной модели данных под трек не требует.
- **`lessons` (ORCH-098)** — журнал ОТКЛОНЕНИЙ, НЕ леджер «прогноз vs факт»: готовой калибровочной
основы под петлю прогноз↔факт (связь с ORCH-8) пока нет.
- **Leaf-паттерн** (`serial_gate`/`coverage_gate`/`labels`/`lessons`): never-raise, kill-switch
`*_enabled`, `*_repos` CSV (**пусто → self-hosting only**), read-only блок в `GET /queue`. Любой
новый модуль оценки обязан ему следовать — это ограничение, а не предмет вопроса.
Без ответов ниже корректные BR/TRZ/AC/тест-план выпустить нельзя: для пунктов Q-2/Q-3 существуют
**взаимоисключающие** варианты спецификации (разные AC, разная модель данных, разный контракт), а
Q-1 определяет, какие из «Шаг 1 / Шаг 2» вообще входят в этот work item.
## 2. Блокирующие вопросы
> Q-1…Q-4 — жёсткие блокеры. Q-5 — вопрос самого Славы с безопасным дефолтом (включён, чтобы
> закрыть все его вопросы за один раунд Needs Input и не плодить второй цикл).
- **Q-1 — Объём и очерёдность: ORCH-020 = «Шаг 1 + Шаг 2» сразу, или «Шаг 1 сейчас» + «Шаг 2 после
ORCH-13»?**
- Вариант A *(рекомендуемый)*: **Шаг 1 (прогноз стоимости/времени) сейчас**; Шаг 2 (адаптивный
выбор модели) — отдельным work item ПОСЛЕ ORCH-13, т.к. без мультипровайдерности «выбор модели»
сводится к выбору эффорта, а сам по себе оценщик сложности можно поставлять как сигнал.
- Вариант B: **оба шага в одном work item** — тогда оценщик сложности обязан выдавать решение,
готовое кормить адаптивный выбор уже сейчас (на тирах эффорта, до ORCH-13).
- Вариант C: **только Шаг 2** (классификация сложности + маппинг), прогноз стоимости/времени — позже.
- Почему блокирует: определяет, выпускаю ли я deliverables на Шаг 2 вообще и какие BR/AC в них
попадут. Объём — зона аналитика, угадывать его нельзя.
- **Q-2 — Адаптивный выбор модели: автоматический по сложности, или с твоим подтверждением для
«дорогих»/нетривиальных?** *(вопрос Славы №3; самый критичный — self-hosting safety)*
- Вариант A *(рекомендуемый)*: **advisory-only** — оценщик лишь предлагает класс/модель (коммент +
карточка), фактический `resolve_agent_model` НЕ трогается; смена модели — ручной/конфиг-шаг.
Прод-control-path неизменен, риск для `enduro-trails` нулевой.
- Вариант B: **авто-override на self-hosting только** — per-task выбор модели/эффорта применяется
автоматически, под kill-switch, скоуп `*_repos` пустой = только `orchestrator`, никогда не влияет
на чужой репозиторий и на уже запущенные задачи.
- Вариант C: **авто для дешёвых тиров, подтверждение для дорогих** (порог по прогнозу $/сложности).
- Почему блокирует: A и B/C — это **разные спецификации** (advisory ⇒ AC про коммент/карточку и
отсутствие правок горячего пути; auto ⇒ AC про безопасный gated override, скоуп, инвариант «не
трогает enduro и in-flight»). Невозможно написать корректные FR/AC, не зная, ввязываемся ли мы в
прод-путь запуска агентов на общем проде.
- **Q-3 — Шкала сложности: фиксированные категории (`trivial/small/medium/complex`) или числовая
(story points)?** *(вопрос Славы №4)*
- Вариант A *(рекомендуемый)*: **фиксированные категории** (как в постановке) — простая модель
данных (`tasks.complexity TEXT`, аддитивно по паттерну `tasks.track`), прозрачный маппинг
«класс → эффорт/модель/трек».
- Вариант B: **числовая** (story points / 1N) — гибче для калибровки, но требует решить пороги
отображения и маппинг диапазон→модель.
- Вариант C: **гибрид** (число внутри + ярлык-категория для людей).
- Почему блокирует: задаёт контракт выхода оценщика, тип новой колонки и форму маппинга
«сложность → модель/трек». Это часть требований (модель данных), не реализационная деталь.
- **Q-4 — Оценка обязательна для КАЖДОЙ задачи, или по запросу / только для крупных?** *(вопрос Славы №2)*
- Вариант A *(рекомендуемый)*: **для каждой задачи автоматически** на входе/в аналитике
(`start_pipeline`), но строго never-raise/best-effort — сбой оценки никогда не тормозит конвейер.
- Вариант B: **по запросу** (эндпоинт/лейбл Plane) и/или только при превышении порога размера.
- Вариант C: **только self-hosting `orchestrator`** на первом этапе (обкатка), enduro — позже.
- Почему блокирует: определяет точку интеграции (триггер в `start_pipeline` для всех проектов
общего прода vs опциональный путь) и формулировку NFR про область раската и обратимость.
- **Q-5 — Где показывать прогноз стоимости/времени: Telegram при заведении, Plane-коммент, оба?**
*(вопрос Славы №1; мягкий — есть безопасный дефолт)*
- Вариант A *(рекомендуемый дефолт)*: **оба** — Plane-коммент (`plane_sync.add_comment`) +
live-карточка/уведомление в Telegram (`notifications.send_telegram`); это безопасный суперсет
поверх уже существующих поверхностей.
- Вариант B: только Telegram. Вариант C: только Plane-коммент.
- Почему (мягко) блокирует: влияет на объём (одна поверхность vs две) и на AC отображения; если
промолчишь — зафиксирую дефолт A.
## 3. Что разблокирует анализ
- **Ответы на Q-1…Q-4** (и подтверждение/override Q-5) комментарием в Plane к ORCH-020.
- Минимально для старта достаточно: **Q-1** (объём), **Q-2** (advisory vs auto — определяет, трогаем
ли control-path) и **Q-3** (шкала). Q-4/Q-5 имеют безопасные дефолты (A), которые я приму при
молчании.
- На **resume**: прочту свежие комментарии-ответы и (а) если все блокеры сняты — выпущу **полный**
валидный пакет из 4 файлов (он автоматически supersedeит этот файл по mtime, повторного Needs
Input не будет); (б) если часть вопросов осталась — **перепишу** этот файл, оставив только
актуальные блокеры (снова Needs Input). Устаревшие вопросы вперемешку с новыми оставлять не буду.
> Подразумеваемые инварианты, которые я зафиксирую в пакете при ЛЮБЫХ ответах (не вопросы — границы):
> новый функционал следует leaf-паттерну (never-raise, kill-switch, `*_repos` пусто → self-hosting
> only, блок в `GET /queue`); `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-ключи/схемы
> существующих таблиц — не трогаются (оценка — наблюдатель/продюсер, **не** Quality Gate); прод-
> контейнер не рестартится; `main`/force-push не затрагиваются. Выбор механизма оценки (эвристика по
> истории / LLM-оценщик / гибрид) и «точность vs стоимость самой оценки» — **архитектурное** решение
> (06-adr), его я не предрешаю; в TRZ зафиксирую лишь требование-ограничение «стоимость оценки ≪
> ожидаемой экономии».

View File

@@ -0,0 +1,166 @@
---
work_item: ORCH-020
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-020 — Оценка задачи, запускаемая Plane-статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное обоснование/решения (выбор механизма оценки эвристика vs LLM vs гибрид, точные
> сигнатуры врезок, индексы, формулы маппинга, сглаживание массовой нагрузки, Plane-группа статуса
> «Оценка») — задача архитектора (`06-adr`).
## 1. Сводка изменения
Вводится **новый операторский Plane-статус «Оценка»** — триггер механизма оценки (по образцу
action-статусов **STOP**/ORCH-090 и **Confirm Deploy**/ORCH-059). Перевод issue в «Оценка»
(в т.ч. **массово** через Plane multi-select) запускает **новый leaf-модуль оценки**
(`src/estimator.py`, never-raise), который прогнозирует **стоимость / время / токены / сложность
(story points `{1,2,3,5,8}`)** на основе истории завершённых задач (агрегаты `src/usage.py`).
Прогноз: (a) пишется в Plane-поле `estimate_point`, (b) публикуется Plane-комментом, (c) добавляется
пунктом «Оценка» (время/токены/стоимость) в общую Telegram-карточку, (d) сохраняется в **новой
аддитивной таблице** `task_estimates` (леджер прогноз↔факт, ключ `work_item_id`). По завершении
оценки оркестратор **возвращает issue в статус `Backlog`**. По завершении самой задачи (переход в
`done`) **факт** пишется в Plane-поле `point`. Пере-оценка — повтор перевода в «Оценка»
(идемпотентно).
**Инвариант (NFR-1/NFR-3):** оценка — наблюдатель/продюсер, **не** Quality Gate и **не** переход
стадии. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схемы существующих
таблиц — байт-в-байт; горячий путь `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — не
трогается. Статус «Оценка» не добавляет ребра в машину стадий.
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `src/plane_sync.py` | **изменить** — (1) `_PLANE_NAME_TO_KEY += {"Оценка": "estimate"}`; ключ `estimate` **НЕ добавлять** в `_DEFAULT_STATES` (fail-closed, как `stop`/`confirm_deploy`); (2) новые write-хелперы `set_issue_estimate_point(work_item, value)`, `set_issue_point(work_item, value)`, `set_issue_backlog(work_item)` (все через guard `_guard_allows_write`, ORCH-117); (3) read-хелпер текущих полей `estimate_point`/`point`. fail-safe при отсутствии estimate-конфига |
| `src/webhooks/plane.py` | **изменить** — в `handle_issue_updated` добавить **fail-closed ветку** `estimate_state = proj_states.get("estimate")``handle_estimate(data, project_id)` (распознаётся как **отдельный** жест, не алиасит stop/to_analyse/confirm_deploy/approved/rejected). Новый `handle_estimate`: резолв issue (pipeline-задачи может не быть), guard `estimator.applies(repo)`, guard «нет активного job» (BR-T6), запуск оценки, затем `set_issue_backlog` |
| `src/estimator.py` | **создать** — leaf: `estimate(work_item_id, issue|description, repo)` → прогноз `{tokens,seconds,cost_usd,story_points}`; маппинг величин → story-point bucket `{1,2,3,5,8}` (чистая функция); расчёт факта из `usage.py`; `applies(repo)`, `should_estimate(task|None)` (анти-disruption), `snapshot()`; never-raise |
| `src/db.py` | **изменить** — аддитивная таблица `task_estimates` (`CREATE TABLE IF NOT EXISTS` в `init_db()`) + хелперы `record_estimate`/`set_actual`/`get_estimate`/`estimates_snapshot`; существующие таблицы/колонки не трогать |
| `src/usage.py` | **переиспользовать** (read-only) — `task_usage_summary`/`agent_cost_totals`/тайминги для **факта**; при необходимости тонкий read-only агрегат «история похожих задач» |
| `src/notifications.py` | **изменить** — пункт «Оценка» (время · токены · стоимость) в рендере общей карточки; never-raise, пустой прогноз → пункт опускается |
| `src/main.py` | **изменить** — (опц.) `POST /estimate?work_item=<id>` / `POST /estimate/backlog` как программное удобство; **read-only блок `estimator` в `GET /queue`** |
| `src/config.py` | **изменить** — флаги (см. §7) |
| `tests/test_orch020_estimator.py` | **создать** — покрытие (см. `04-test-plan.yaml`) |
## 3. Функциональные требования
### FR-T1 — Статус «Оценка» как триггер (BR-T1, BR-T5)
`_PLANE_NAME_TO_KEY["Оценка"] = "estimate"`; ключ `estimate` **отсутствует** в `_DEFAULT_STATES`.
В `handle_issue_updated` — отдельная ветка: `estimate_state = proj_states.get("estimate")`;
`if estimate_state and new_state == estimate_state: await handle_estimate(...)`. Доска без статуса →
`estimate_state is None` → ветка инертна (fail-closed, зеркало `stop`/`confirm_deploy`). Ветка не
должна аннулировать/перехватывать STOP/`to_analyse`/`confirm_deploy`/approved/rejected (UUID
«Оценка» отличен от всех; порядок ветки выбирает архитектор, инвариант — взаимоисключение жестов).
### FR-T2 — Обработчик `handle_estimate` (BR-T1, BR-T6)
`handle_estimate(data, project_id)`: резолвит `plane_id`/`work_item_id`; `repo` определяется по
проекту. Guard-цепочка (все — no-op-with-log при невыполнении, never-raise):
1. `estimator.applies(repo)` — kill-switch + скоуп (False → no-op);
2. **анти-disruption (BR-T6):** если у issue есть pipeline-задача с **активным** job
(`has_active_job_for_task`) → no-op + лог (не выдёргивать in-flight работу). Issue без задачи
(бэклог) или с терминальной/idle-задачей → оценка допустима.
Далее: `estimator.estimate(...)` → запись прогноза (FR-T3) → **`set_issue_backlog(work_item)`**
(BR-T2). Контракт never-raise: любая ошибка логируется, вебхук-флоу не падает.
### FR-T3 — Прогноз задачи (BR-1, BR-2, BR-3)
`estimator.estimate(work_item_id, description|issue, repo)` возвращает `{forecast_tokens,
forecast_seconds, forecast_cost_usd, story_points}`, `story_points ∈ {1,2,3,5,8}`. База — история
похожих **завершённых** задач (средние токены/время/стоимость из `usage.py`-агрегатов); пустая
история → bootstrap-дефолт. Маппинг величин → bucket — чистая функция (пороги — `06-adr`).
never-raise: сбой → безопасный дефолт + warning.
### FR-T4 — Семантика story points (BR-3)
Шкала фиксированная: `1` docs/label/config · `2` небольшой фикс · `3` средняя · `5` сложная
(код+тесты) · `8` эпик/разбивать. Значения вне набора не выдаются.
### FR-T5 — Авто-возврат в Backlog + анти-loop (BR-T2, BR-T6)
После оценки `handle_estimate` зовёт `set_issue_backlog(work_item)` → issue возвращается в `Backlog`.
Это **не** создаёт цикла: `Backlog`-UUID не совпадает ни с одной триггер-веткой `handle_issue_updated`
(`stop`/`to_analyse`/`confirm_deploy`/`approved`/`rejected`/`estimate`) → входящий webhook «state →
Backlog» = no-op-эхо. Возврат best-effort: сбой записи статуса не роняет флоу (прогноз уже записан).
### FR-T6 — Массовость и пере-оценка (BR-T3, BR-T4)
Массовый перевод N задач в «Оценка» = N независимых `issue.updated`-вебхуков → N вызовов
`handle_estimate` (никакого спец-batch-кода). Пере-оценка = повторный перевод: `estimate`
идемпотентно **перезаписывает** прогноз в `task_estimates` (UPSERT по `work_item_id`) и
`estimate_point`; дублей строк нет.
### FR-T7 — Запись прогноза и факта в Plane (BR-7, BR-8, NFR-6, NFR-7)
- Прогноз story points → `set_issue_estimate_point` → поле issue `estimate_point`.
- По завершении задачи (переход в `done`, врезка в существующий done-путь): из `usage.py` считается
факт (токены/время/стоимость) → маппится в story-point bucket → `set_issue_point` → поле `point`;
`estimate_point` не перезаписывается.
- Все записи через `plane_sync` под guard ORCH-117; отсутствие estimate-конфига/поля → best-effort
пропуск + лог (не падать).
### FR-T8 — Отображение (BR-9)
- **Plane-коммент** с прогнозом (стоимость/время/токены/story points) — `plane_sync.add_comment`.
- **Telegram-карточка** — пункт **«Оценка»**: время · токены · стоимость (`notifications`).
Обе поверхности — best-effort, не блокируют конвейер.
### FR-T9 — Леджер прогноз↔факт (BR-10)
`task_estimates` хранит прогноз (на момент оценки) и факт (на момент `done`) + дельту, ключ
`work_item_id` (т.к. на момент оценки `task_id` может быть `NULL` — issue на бэклоге). Фундамент
калибровки (ORCH-8); авто-уточнение модели в объём не входит.
### FR-T10 — leaf-инварианты (NFR-2, NFR-3)
`applies(repo)` = `estimator_enabled` ∧ скоуп `estimator_repos` (пусто → self-hosting only),
проверяется локально и ПЕРВЫМ (без сети). Выключено → весь модуль инертен (нулевая регрессия:
статус «Оценка» не обрабатывается, ничего не пишется). read-only блок `estimator` в `GET /queue`
(флаг/скоуп/счётчики прогнозов/записей/возвратов-в-Backlog).
## 4. Изменения API
| Метод/путь | Назначение |
|------------|-----------|
| **Plane-статус «Оценка»** (не HTTP-эндпоинт) | **Основной триггер**: перевод issue в статус → `handle_estimate`. Массовость — multi-select Plane. |
| `POST /estimate?work_item=<id>` *(опц.)* | Программно произвести/обновить прогноз одной задачи (то же ядро, что статус-триггер) — удобство/диагностика, не основной путь |
| `POST /estimate/backlog` *(опц.)* | Программно оценить backlog-задачи проекта — удобство; основной массовый путь — статус «Оценка» |
| `GET /estimate?work_item=<id>` *(опц.)* | Прочитать текущий прогноз vs факт из `task_estimates` |
| `GET /queue` | **+ read-only блок `estimator`**; existing-поля не меняются |
Существующие эндпоинты/контракты не изменяются. Webhook-контракт `issue.updated` не меняется —
добавляется лишь распознавание ещё одного целевого статуса.
## 5. Изменения схемы БД
**Новая аддитивная таблица** `task_estimates` (`CREATE TABLE IF NOT EXISTS`, без правки существующих):
`work_item_id` (ключ/UPSERT-цель) · `task_id` (нуллабелен до старта пайплайна) · `repo` · прогноз
(`forecast_tokens`, `forecast_seconds`, `forecast_cost_usd`, `forecast_story_points`) · факт
(`actual_tokens`, `actual_seconds`, `actual_cost_usd`, `actual_story_points`) · дельта (`delta_*`
или вычисляемая) · `source` (`status`/`manual`/`api`) · `estimate_count` (число пере-оценок,
опц.) · `created_at` · `updated_at`. Точные типы/индексы/уникальность (UNIQUE по `work_item_id`
для идемпотентного UPSERT) — `06-adr`. Существующие таблицы (`tasks`/`agent_runs`/`jobs`/…) — **не
изменяются** (NFR-8).
## 6. Требования к новым/изменённым QG checks
**Нет.** Оценка — наблюдатель/продюсер, не Quality Gate; статус «Оценка» — операторский side-триггер,
не ребро `STAGE_TRANSITIONS`. `QG_CHECKS` / `check_*` / machine-verdict-ключи / `STAGE_TRANSITIONS`
**не трогаются**. Новых номерных артефактов pipeline (`NN-*.md`) и новых вердикт-парсеров нет (оценка
публикуется в Plane/Telegram/`task_estimates`, не во frontmatter-гейтах).
## 7. Совместимость / регресс
- **Флаги** (`config.py`, дефолты безопасные): `estimator_enabled` (kill-switch, env
`ORCH_ESTIMATOR_ENABLED`), `estimator_repos` (CSV, env `ORCH_ESTIMATOR_REPOS`; **пусто →
self-hosting only**). Доп. тюнинг (bootstrap-дефолты, пороги bucket, целевой возврат-статус,
сглаживание массовой нагрузки) — конфиг-ключи на усмотрение `06-adr`.
- **Откат** = `ORCH_ESTIMATOR_ENABLED=false` → модуль инертен: статус «Оценка» не обрабатывается
(`applies`=False до сети), ни записи в Plane, ни строки карточки, ни обращений к таблице; конвейер
байт-в-байт до ORCH-020. Доп. откат «на уровне доски» — не создавать статус «Оценка» (fail-closed,
BR-T5).
- **Область раската:** по умолчанию self-hosting `orchestrator`; `enduro-trails` не затронут (скоуп
`estimator_repos` пуст + на его доске статуса «Оценка» нет → fail-closed).
- **never-raise / fail-safe:** все публичные функции и врезки изолированы (`try/except` → warning +
безопасный дефолт). Сбой оценки/записи в Plane/возврата статуса/рендера карточки — не роняет
конвейер (NFR-2/6/7).
- **Анти-disruption / анти-loop:** активный job → no-op (BR-T6); возврат в Backlog — no-op-эхо
(FR-T5). Машина стадий и in-flight задачи не затрагиваются.
- **Горячий путь не тронут:** `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — без изменений
(NFR-3).
- **Инфра-предусловия (NFR-7):** (a) статус **«Оценка»** на доске проекта (онбординг ORCH-009 →
23-й статус; группа — `06-adr`/`07-infra-requirements.md`); (b) estimate-система Plane
(`1/2/3/5/8`) для `estimate_point`; их отсутствие → fail-closed/best-effort пропуск, не падение.

View File

@@ -0,0 +1,221 @@
---
work_item: ORCH-020
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-020 — Оценка задачи, запускаемая статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
считается провалом). Reviewer/тестер проверяет их буквально по файлам репозитория и поведению.
---
## AC-T1 — Запуск оценки статусом «Оценка» (ядро ревизии)
**Условие:** перевод issue в Plane-статус «Оценка» запускает оценку этой задачи.
- **PASS:** `_PLANE_NAME_TO_KEY` содержит `"Оценка" → "estimate"`; `handle_issue_updated` имеет
отдельную ветку `proj_states.get("estimate")``handle_estimate(...)`; при переводе issue в
«Оценка» вызывается оценка (прогноз вычислен и записан).
- **FAIL:** триггера-статуса нет; оценка по-прежнему авто-запускается на каждой задаче в
`start_pipeline`; ветка «Оценка» аннулирует/перехватывает STOP/`to_analyse`/`confirm_deploy`/
approved/rejected.
---
## AC-T2 — Авто-возврат в Backlog
**Условие:** по завершении оценки issue возвращается в статус `Backlog`.
- **PASS:** после записи прогноза `handle_estimate` вызывает `set_issue_backlog(work_item)` и issue
оказывается в `Backlog`; возврат best-effort (сбой записи статуса не роняет флоу, прогноз уже
записан).
- **FAIL:** issue остаётся в «Оценка»/ином статусе; возврат отсутствует; сбой возврата роняет вебхук.
---
## AC-T3 — Массовость через Plane
**Условие:** массовый перевод задач в «Оценка» оценивает их все.
- **PASS:** N задач, переведённых в «Оценка» (multi-select Plane → N `issue.updated`-вебхуков),
дают N независимых вызовов `handle_estimate`; каждая получает прогноз; спец-batch-кода для этого не
требуется.
- **FAIL:** часть задач не оценивается; обработка зависит от несуществующего «batch-режима»; один
webhook гасит остальные.
---
## AC-T4 — Пере-оценка много раз (идемпотентно)
**Условие:** повторный перевод в «Оценка» переоценивает задачу без дублей.
- **PASS:** повтор обновляет прогноз в `task_estimates` (UPSERT по `work_item_id`) и `estimate_point`;
строка одна, не дублируется; число пере-оценок не ограничено.
- **FAIL:** повтор создаёт дубль строки в `task_estimates`; повтор игнорируется/падает.
---
## AC-T5 — Fail-closed статус «Оценка»
**Условие:** на доске без статуса «Оценка» триггер не активируется.
- **PASS:** `estimate` отсутствует в `_DEFAULT_STATES`; на проекте без статуса
`proj_states.get("estimate") is None` → ветка инертна (нет KeyError, нет оценки); enduro-trails не
затронут.
- **FAIL:** `estimate` добавлен в `_DEFAULT_STATES`; отсутствие статуса даёт KeyError/ошибку; чужой
репо триггерится.
---
## AC-T6 — Анти-disruption in-flight + анти-loop
**Условие:** статус «Оценка» — side-механизм, не трогает выполняемую работу и не зацикливается.
- **PASS:** issue с активным (queued/running) job → `handle_estimate` = no-op + лог (in-flight работа
не выдёргивается в Backlog, стадии не трогаются); возврат в `Backlog` — no-op-эхо (`Backlog`-UUID не
совпадает ни с одной триггер-веткой → входящий webhook ничего не запускает).
- **FAIL:** активную задачу выдёргивает в Backlog/прерывает; возврат в Backlog порождает повторный
запуск оценки (цикл); меняется `STAGE_TRANSITIONS`.
---
## AC-1 — Прогноз четырёх величин
**Условие:** `estimator.estimate(...)` возвращает прогноз стоимости, времени, токенов и сложности.
- **PASS:** структура с `forecast_cost_usd`, `forecast_seconds`, `forecast_tokens` и `story_points`,
`story_points ∈ {1,2,3,5,8}`; пустая история → bootstrap-дефолт (не исключение).
- **FAIL:** отсутствует любая из четырёх величин; `story_points` вне `{1,2,3,5,8}`; функция бросает
исключение при отсутствии истории.
---
## AC-2 — Фиксированная семантика story points
**Условие:** маппинг величин → story-point bucket соответствует шкале заказчика.
- **PASS:** значения и смысл строго `1` (docs/label/config) · `2` (небольшой фикс) · `3` (средняя) ·
`5` (сложная код+тесты) · `8` (эпик/разбивать); чистая функция маппинга покрыта unit-тестом.
- **FAIL:** иные значения/градации (`4`, `7`, свободное число) или произвольная числовая шкала.
---
## AC-3 — Запись прогноза в Plane `estimate_point`
**Условие:** прогноз story points записывается в поле issue `estimate_point`.
- **PASS:** при оценке вызывается `set_issue_estimate_point` (через `plane_sync`/guard ORCH-117); при
настроенной estimate-системе значение оказывается в `estimate_point`.
- **FAIL:** прогноз пишется в `point` (перепутаны поля), не пишется, либо запись обходит guard.
---
## AC-4 — Запись факта в Plane `point` по завершении
**Условие:** по завершении задачи (переход в `done`) факт пишется в смежное поле `point`.
- **PASS:** на `done` факт вычисляется из `usage.py` (токены/время/стоимость), маппится в story-point
bucket и пишется в `point`; `estimate_point` не перезаписывается.
- **FAIL:** факт пишется в `estimate_point`, не пишется, либо затирает прогноз.
---
## AC-5 — Пункт «Оценка» в Telegram-карточке
**Условие:** общая карточка задачи показывает прогноз.
- **PASS:** в карточке присутствует пункт **«Оценка»** с **временем, токенами и стоимостью**; пустой
прогноз → пункт опускается (never-raise); инвариант «одна карточка на задачу» не нарушен.
- **FAIL:** пункт отсутствует; его рендер роняет/ломает карточку; нарушен инвариант одной карточки.
---
## AC-6 — Plane-коммент с прогнозом
**Условие:** прогноз публикуется комментом в Plane.
- **PASS:** постится коммент со стоимостью/временем/токенами/story points (best-effort, через
`add_comment`).
- **FAIL:** коммент не постится при включённом флаге на применимом репо без причины в логе.
---
## AC-7 — Программные эндпоинты (опциональны, не основной триггер)
**Условие:** программный путь, если реализован, использует то же ядро.
- **PASS:** `POST /estimate?work_item=<id>` / `POST /estimate/backlog` (если есть) дают тот же
результат, что статус-триггер (UPSERT в `task_estimates` + `estimate_point` + коммент + карточка),
идемпотентны; их отсутствие не нарушает приёмку (основной путь — статус «Оценка»).
- **FAIL:** эндпоинт расходится с поведением статус-триггера; преподносится как ЕДИНСТВЕННЫЙ способ
запуска (триггер-статуса нет).
---
## AC-8 — On-demand + доступность до старта, best-effort
**Условие:** оценка запускается по требованию (статус), доступна до старта работы и никогда не
блокирует конвейер.
- **PASS:** оценка идёт по переводу в «Оценка» на бэклоге (до `To Analyse`/`start_pipeline`); при
сбое оценки конвейер не затрагивается (best-effort, лог-warning); НЕ авто-обязательна на каждой
задаче.
- **FAIL:** оценка — блокирующий шаг (сбой тормозит/меняет маршрут); оценка авто-навязана каждой
задаче на `start_pipeline`.
---
## AC-9 — leaf-инварианты (kill-switch / скоуп / GET /queue)
**Условие:** модуль следует leaf-паттерну.
- **PASS:** `estimator_enabled=false` → модуль полностью инертен (статус «Оценка» не обрабатывается,
нет записей в Plane/карточку/таблицу); `estimator_repos` пуст → активен только на self-hosting
`orchestrator`; есть read-only блок `estimator` в `GET /queue`; все публичные функции never-raise.
- **FAIL:** при выключенном флаге что-то пишется/меняется; enduro-trails затронут при пустом скоупе;
нет блока в `GET /queue`; функция бросает наружу.
---
## AC-10 — Control-path и гейты не тронуты (NFR-1/NFR-3)
**Условие:** оценка ничего не меняет в машине стадий и горячем пути.
- **PASS:** `git diff` не затрагивает `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict-
ключи и схемы существующих таблиц; `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — без
изменений; статус «Оценка» не добавлен как ребро стадий; зелёный анти-регресс существующих тестов.
- **FAIL:** любое из перечисленного изменено; маршрут задачи зависит от результата оценки.
---
## AC-11 — Шаг 2 (выбор модели) вне объёма
**Условие:** адаптивный выбор модели не реализуется.
- **PASS:** нет кода, выбирающего/меняющего модель/эффорт по сложности; в `01-brd.md` зафиксирован
out-of-scope + follow-up на отдельный work item с зависимостью на ORCH-13.
- **FAIL:** добавлена логика per-task override модели/эффорта; follow-up не зафиксирован.
---
## AC-12 — Леджер прогноз↔факт + fail-safe записи
**Условие:** прогноз и факт сохраняются; запись в Plane fail-safe.
- **PASS:** `task_estimates` (новая аддитивная таблица, ключ `work_item_id`, `task_id` нуллабелен)
хранит прогноз, факт и дельту; при ненастроенной estimate-системе Plane запись `estimate_point`/
`point` best-effort пропускается с логом, конвейер не падает.
- **FAIL:** существующая схема БД изменена; отсутствие estimate-конфига роняет оценку/конвейер.
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-T1 | BR-T1, BR-T5 / FR-T1 |
| AC-T2 | BR-T2 / FR-T5 |
| AC-T3 | BR-T3 / FR-T6 |
| AC-T4 | BR-T4 / FR-T6 |
| AC-T5 | BR-T5 / FR-T1 |
| AC-T6 | BR-T6 / FR-T2, FR-T5 |
| AC-1 | BR-1, BR-2 / FR-T3 |
| AC-2 | BR-3 / FR-T4 |
| AC-3 | BR-7 / FR-T7 |
| AC-4 | BR-8 / FR-T7 |
| AC-5 | BR-9 / FR-T8 |
| AC-6 | BR-9 / FR-T8 |
| AC-7 | §2 «Вне объёма» / FR-T6, TRZ §4 |
| AC-8 | BR-4, BR-5 / FR-T2 |
| AC-9 | NFR-2 / FR-T10 |
| AC-10 | NFR-1, NFR-3 |
| AC-11 | §2 «Вне объёма» (Q-1/Q-2) |
| AC-12 | BR-10, NFR-7, NFR-8 / FR-T7, FR-T9 |

View File

@@ -0,0 +1,149 @@
work_item: ORCH-020
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
title: "Оценка задачи, запускаемая Plane-статусом «Оценка»: триггер/возврат в Backlog/массовость/пере-оценка + прогноз {токены,время,стоимость,story points}, запись в Plane, карточка, леджер прогноз↔факт, leaf-инварианты"
framework: pytest
scope: >
Покрывается: распознавание статуса «Оценка» как триггера (handle_estimate),
fail-closed при отсутствии статуса, авто-возврат issue в Backlog + анти-loop,
анти-disruption in-flight (no-op при активном job), массовость (N вебхуков -> N оценок),
идемпотентная пере-оценка (UPSERT по work_item_id), расчёт прогноза из истории (usage-агрегаты),
маппинг величин -> story-point bucket {1,2,3,5,8} (чистая функция), never-raise/bootstrap при
пустой истории, запись прогноза в estimate_point и факта в point (через guard ORCH-117, fail-safe
при отсутствии estimate-конфига), пункт "Оценка" в Telegram-карточке, read-only блок estimator в
GET /queue, аддитивная таблица task_estimates (ключ work_item_id, task_id нуллабелен),
kill-switch + скоуп (пусто -> self-hosting only).
Вне покрытия: адаптивный выбор модели (Шаг 2, вне объёма), авто-уточнение модели оценки (ORCH-8),
автопереключение трека по сложности (ORCH-19).
notes: >
Тесты используют изолированную временную SQLite-БД (фикстура init_db во временном файле) и
замоканные plane_sync/notifications/usage/get_project_states — без сети, без боевого Plane/Telegram,
без LLM. Триггер тестируется на уровне handle_issue_updated/handle_estimate с подставленными
proj_states (UUID статуса "Оценка"). Запись в Plane проверяется на уровне вызова write-хелперов под
guard (ORCH-117 autouse-floor conftest держит opt-in OFF — сетевая запись физически невозможна из
теста). Control-path анти-регресс: STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схемы
существующих таблиц не меняются; полный регресс tests/ остаётся зелёным.
tests:
- id: TC-01
type: integration
description: "Триггер: new_state == proj_states['estimate'] -> handle_estimate вызывается; estimate-статус добавлен в _PLANE_NAME_TO_KEY как 'Оценка'->'estimate' (AC-T1)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-02
type: integration
description: "Fail-closed: 'estimate' отсутствует в _DEFAULT_STATES; на проекте без статуса proj_states.get('estimate') is None -> ветка инертна, handle_estimate не зовётся, нет KeyError (AC-T5)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-03
type: integration
description: "handle_estimate на backlog-issue (нет pipeline-задачи): прогноз вычислен, записан, затем set_issue_backlog -> issue возвращён в Backlog (AC-T1, AC-T2)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-04
type: integration
description: "Анти-disruption: issue с активным job (has_active_job_for_task=True) -> handle_estimate no-op + лог, оценка не запускается, статус не меняется (AC-T6)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-05
type: integration
description: "Анти-loop: возврат в Backlog не алиасит триггер-ветки (Backlog-UUID != estimate/stop/to_analyse/confirm_deploy/approved/rejected) -> входящий 'state->Backlog' webhook = no-op-эхо (AC-T6)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-06
type: integration
description: "Массовость: N issue.updated со state='Оценка' -> N независимых вызовов handle_estimate, каждый даёт прогноз; один webhook не гасит остальные (AC-T3)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-07
type: integration
description: "Идемпотентная пере-оценка: повторный перевод в 'Оценка' -> UPSERT по work_item_id обновляет одну строку task_estimates и estimate_point, не дублирует (AC-T4)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-08
type: unit
description: "estimate() возвращает {forecast_tokens,forecast_seconds,forecast_cost_usd,story_points}, story_points в {1,2,3,5,8} (AC-1)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-09
type: unit
description: "Маппинг величин -> story-point bucket: точная семантика 1/2/3/5/8 на граничных входах (AC-2)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-10
type: unit
description: "Пустая история -> bootstrap-дефолт, не исключение; estimate() never-raise при битых данных (AC-1, AC-9)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-11
type: unit
description: "Расчёт факта на done из usage-агрегатов (токены/время/стоимость) маппится в story-point bucket (AC-4)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-12
type: integration
description: "Прогноз пишется в estimate_point через set_issue_estimate_point; факт — в point через set_issue_point; поля не перепутаны, прогноз не затирается (AC-3, AC-4)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-13
type: integration
description: "Telegram-карточка содержит пункт 'Оценка' (время/токены/стоимость); пустой прогноз -> пункт опускается, карточка не падает (AC-5)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-14
type: integration
description: "Plane-коммент с прогнозом постится через add_comment (best-effort) (AC-6)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-15
type: unit
description: "kill-switch estimator_enabled=false -> модуль инертен (handle_estimate no-op, нет записей в Plane/карточку/таблицу); applies() локален и first (AC-9)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-16
type: unit
description: "Скоуп estimator_repos пуст -> активен только self-hosting orchestrator; enduro-trails -> no-op (AC-9)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-17
type: integration
description: "GET /queue содержит read-only блок estimator (флаг/скоуп/счётчики прогнозов/записей/возвратов); existing-поля не меняются (AC-9)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-18
type: unit
description: "Аддитивная таблица task_estimates: CREATE TABLE IF NOT EXISTS идемпотентна; record_estimate/set_actual/get_estimate хранят прогноз+факт+дельту с ключом work_item_id (task_id нуллабелен); существующие таблицы не изменены (AC-12)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-19
type: integration
description: "fail-safe записи в Plane: estimate-система не настроена -> set_issue_estimate_point/point best-effort пропуск + лог, без падения; авто-возврат в Backlog всё равно отрабатывает (AC-12, AC-T2, NFR-7)"
module: tests/test_orch020_estimator.py
expected: PASS
- id: TC-20
type: unit
description: "Анти-регресс control-path: STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict-ключи, resolve_agent_model/resolve_agent_effort не изменены; статус 'Оценка' не добавлен как ребро стадий (AC-10, AC-11)"
module: tests/test_orch020_estimator.py
expected: PASS

View File

@@ -0,0 +1,262 @@
---
work_item: ORCH-020
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# ADR-001: Оценка задачи как side-механизм, запускаемый операторским Plane-статусом «Оценка», с детерминированной эвристикой по истории
Work Item: **ORCH-020** — Оценка задачи (прогноз стоимости/времени/токенов/story points), запускаемая статусом «Оценка»
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0054-task-estimation-status-trigger.md`** (решение кросс-каттинговое: новый член семейства операторских action-статусов + новая аддитивная таблица + новый primitive записи в Plane + новый leaf).
## Статус
Proposed
## Контекст
BRD/TRZ (`01-brd.md`/`02-trz.md`, ревизия после REJECT 2026-06-17) требуют: оператор **массово**
переводит backlog-задачи в выделенный Plane-статус **«Оценка»**, по каждой оркестратор прогнозирует
**стоимость / время / токены / сложность (story points `{1,2,3,5,8}`)**, пишет прогноз в Plane-поле
`estimate_point`, публикует в Plane-комменте и пункте «Оценка» Telegram-карточки, сохраняет в леджер
прогноз↔факт и **возвращает issue в Backlog**; пере-оценка повтором идемпотентна; по завершении задачи
факт пишется в `point`. Шаг 2 (адаптивный выбор модели) — **вне объёма** (заказчик: «Модели не выбираем
и не меняем»).
Факты, сверенные с кодом (не изобретать):
- **Семейство операторских action-статусов уже существует.** `webhooks/plane.py::handle_issue_updated`
(строки 163181) разбирает STOP (ORCH-090) и Confirm Deploy (ORCH-059) через `proj_states.get("<key>")`;
оба **намеренно отсутствуют** в `plane_sync._DEFAULT_STATES` (fail-closed) и сопоставляются именем через
`_PLANE_NAME_TO_KEY` (`src/plane_sync.py:131`). Статус «Оценка» — третий представитель того же семейства.
- **Массовость «бесплатна»:** Plane multi-select → N независимых `issue.updated`-вебхуков; спец-batch-кода не нужно.
- **Фактура для калибровки накоплена:** `usage.task_usage_summary(task_id)` (`src/usage.py:834`) агрегирует
токены/стоимость per-task из `agent_runs`; тайминги — `tasks.created_at/updated_at`,
`agent_runs.started_at/finished_at`. Колонка `tasks.track` (ORCH-019) различает `full`/`bug`.
- **Запись в Plane идёт через guard ORCH-117:** все три примитива записи (`update_issue_state`/`add_comment`/
`_set_issue_state_direct`) проходят `_guard_allows_write` (`src/plane_sync.py:847`) — из тест/worktree-процесса
запись в боевой проект физически заблокирована.
- **estimate-система Plane не настроена** на момент анализа; `estimate_point` — FK на estimate-point estimate-системы,
`point` — целочисленное поле issue. В `src/` **нет** кода работы с Plane-estimate (net-new интеграция).
- **leaf-паттерн платформы** (`serial_gate`/`coverage_gate`/`bug_fast_track`/`lessons`): never-raise, kill-switch
`*_enabled`, скоуп `*_repos` (пусто → self-hosting only через `qg.checks.is_self_hosting_repo`), read-only блок
в `GET /queue`, `applies(repo)` локально и ПЕРВЫМ.
- **Хук done-факта:** блок `if next_stage == "done"` в `stage_engine.advance_stage` (`src/stage_engine.py:521`)
— единственная авторитетная точка перехода в терминал.
**Инвариант (NFR-1/NFR-3):** оценка — наблюдатель/продюсер, **не** Quality Gate и **не** переход стадии.
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схемы существующих таблиц —
байт-в-байт. Горячий путь `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — не трогается.
## Решение
### Сводка
Вводим **новый операторский Plane-статус «Оценка»** как третий член семейства action-статусов
(STOP/Confirm Deploy) — fail-closed `.get("estimate")`-ветка в `handle_issue_updated`, делегирующая
**новому leaf-модулю `src/estimator.py`** (never-raise, kill-switch, скоуп). Механизм прогноза —
**детерминированная эвристика по истории завершённых задач** (чистые функции, **без LLM-вызова**):
прогноз = средние токены/время/стоимость похожих `done`-задач того же репо/трека, story-points —
чистая функция-бакетизатор. Прогноз пишется в Plane (`estimate_point` + коммент), Telegram-карточку и
**новую аддитивную таблицу `task_estimates`** (UPSERT по `work_item_id`); затем issue возвращается в
`Backlog`. По завершении задачи факт (из `usage.py`) пишется в `point` и в леджер. Всё — аддитивно,
под флагами, fail-safe, без касания control-path.
### D1 — Механизм прогноза: детерминированная эвристика по истории, **без LLM** (NFR-4, NFR-5; решение Q открытого вопроса TRZ §NFR-4)
Это **главное архитектурное решение**, которое TRZ явно делегировал архитектору.
- **Выбор: чистая детерминированная эвристика** (in-process, без сетевого LLM-вызова и без субпроцесса).
Прогноз вычисляется парой индексированных SQL-чтений + чистыми функциями за микросекунды.
- **Почему не LLM-оценщик / не гибрид (на этом шаге):**
1. **NFR-5 (массовость).** Multi-select десятков задач → десятки почти одновременных вебхуков. LLM-вызов
на оценку умножился бы на N и конкурировал бы за **единственный транспорт LLM** (`launcher._spawn`) с
боевыми агентами, рискуя замедлить обслуживание **других** проектов (enduro) из общего прода.
2. **NFR-4 (стоимость ≪ ценности).** Opus-вызов на каждую из десятков backlog-задач — это реальные $ за
саму оценку; эвристика бесплатна.
3. **Политика ORCH-118 (determinization-roadmap).** Платформа целенаправленно **сокращает** avoidable
LLM-пути (`llm-usage-policy.md`: «LLM — только где нужно настоящее суждение»). Оценка размера по истории
— деривируемая из tool-сигналов величина, **не** требующая суждения LLM. Вводить здесь новый LLM-путь
прямо противоречит действующей политике.
4. **Воспроизводимость/тестируемость.** Детерминированный бакетизатор покрывается unit-тестами на границах
(AC-2 / TC-09), чего LLM не даёт.
- **Стек-расширяемость (BR-6 содержания, без реализации сейчас):** контракт `estimator.estimate(work_item_id,
description|issue, repo) -> {forecast_tokens, forecast_seconds, forecast_cost_usd, story_points}` —
**граница расширения**. Будущий гибридный LLM-рефайнер (если когда-нибудь понадобится) встраивается
ЗА этой границей без изменения вызывающих. Сейчас LLM-рефайнер **не строится** (Шаг 2 / выбор модели вне
объёма, AC-11).
### D2 — Модель прогноза: средние по «похожим» завершённым задачам + bootstrap (BR-1, BR-2)
- **«Похожие» = тот же `repo` И тот же `track`** (`full`/`bug`, ORCH-019) среди задач со `stage='done'`.
Трек — дешёвый, уже хранимый, осмысленный разрез сложности (багфикс короче полного цикла).
- **Источник фактуры (read-only):** тонкий агрегат `db.completed_task_stats(repo, track) ->
{n, mean_tokens, mean_cost_usd, mean_seconds}` поверх `agent_runs` (токены/стоимость, как
`task_usage_summary`, но сгруппировано по завершённым задачам) и `tasks` (время = `updated_at - created_at`,
отсечка аномалий по `estimator_wall_cap_s`, зеркало `tracker_brd_review_cap_s` ORCH-087). `usage.py`
переиспользуется read-only.
- **Прогноз = средние** по выборке. `forecast_tokens = mean_tokens`, `forecast_cost_usd = mean_cost_usd`,
`forecast_seconds = mean_seconds`.
- **Bootstrap (пустая/малая история):** `n < estimator_min_samples` (дефолт 3) → значения берутся из
конфиг-дефолтов `estimator_bootstrap_{tokens,cost_usd,seconds}` (или смешиваются с имеющейся выборкой —
деталь реализации; разработчику разрешена линейная интерполяция). **Никогда не исключение** (AC-1/TC-10).
- **Сигналы описания (опц., v1 — не обязательны):** длина текста постановки / наличие метки `Bug` могут
скорректировать выбор трека; в v1 достаточно `repo+track`. Расширение — за границей D1.
### D3 — Бакетизатор story points: чистая функция, конфигурируемые пороги (BR-3, AC-2)
- **Чистая функция** `estimator.story_points_for(forecast) -> int ∈ {1,2,3,5,8}`. Первичный сигнал —
**`forecast_cost_usd`** (прямая ось «сколько будет стоить», запрошенная заказчиком; легко
ре-калибруется конфигом при смене тарифа/провайдера ORCH-13).
- **Пороги** — конфиг `estimator_sp_cost_thresholds` (CSV из 4 возрастающих кат-оффов `t1,t2,t3,t5`),
семантика `<=` по возрастанию:
`cost ≤ t1 → 1` · `≤ t2 → 2` · `≤ t3 → 3` · `≤ t5 → 5` · `иначе → 8`.
**Дефолты (bootstrap, подлежат калибровке):** `0.50, 2.00, 5.00, 12.00` ($).
- **Семантика шкалы (фиксирована, BR-3/FR-T4):** `1` docs/label/config · `2` небольшой фикс · `3` средняя ·
`5` сложная (код+тесты) · `8` эпик/разбивать. Значения вне `{1,2,3,5,8}` не выдаются.
- **Факт-story-points** считаются той же функцией по фактической стоимости (консистентность прогноз↔факт).
- Калибровка порогов — задача петли ORCH-8 поверх леджера D7; пороги конфигурируемы именно ради этого.
### D4 — Триггер: fail-closed ветка `estimate`, взаимоисключение жестов (BR-T1, BR-T5, AC-T1, AC-T5)
- `plane_sync._PLANE_NAME_TO_KEY["Оценка"] = "estimate"`; ключ `estimate` **НЕ добавляется** в
`_DEFAULT_STATES` (fail-closed, как `stop`/`confirm_deploy`). На доске без статуса
`proj_states.get("estimate") is None` → ветка инертна (нет KeyError, нет оценки).
- В `handle_issue_updated` — отдельная ветка `estimate_state = proj_states.get("estimate")`;
`if estimate_state and new_state == estimate_state: await handle_estimate(...)`. **Размещение:** сразу
после ветки `stop` (раннее, рядом с прочими `.get`-action-статусами). Корректность взаимоисключения
обеспечена **различием UUID** статусов (а не порядком); порядок выбран для читаемости. Ветка не
алиасит STOP/`to_analyse`/`confirm_deploy`/`approved`/`rejected`.
### D5 — `handle_estimate`: анти-disruption, авто-возврат, анти-loop (BR-T2, BR-T6, FR-T2, FR-T5, AC-T2, AC-T6)
- `handle_estimate(data, project_id)` резолвит `plane_id`/`work_item_id`; `repo` — по проекту
(`projects.get_project_by_repo`/реестр). **Исполнение off-loop** через `asyncio.to_thread` (зеркало
`handle_stop`), т.к. ядро синхронно и делает сетевые Plane-вызовы. Контракт never-raise.
- **Guard-цепочка (каждый — no-op-with-log при невыполнении):**
1. `estimator.applies(repo)` — kill-switch + скоуп, локально и ПЕРВЫМ (без сети при выключенном флаге);
2. **анти-disruption (BR-T6):** issue с pipeline-задачей, у которой есть **активный** job
(`db.has_active_job_for_task(task_id)`, `src/db.py:1323`) → no-op + лог (не выдёргивать in-flight
работу). Backlog-issue (нет задачи) или терминальная/idle-задача → оценка допустима.
- Далее: `estimator.estimate(...)` → запись прогноза (D6/D7/D8) → **`set_issue_backlog(work_item)`** (D6).
- **Анти-loop:** `backlog` не совпадает ни с одной триггер-веткой → входящий «state→Backlog» webhook —
no-op-эхо. Возврат best-effort: сбой записи статуса не роняет флоу (прогноз уже записан).
### D6 — Запись в Plane: `estimate_point` (FK) + `point` (int) + коммент + Backlog (BR-7, BR-8, FR-T7, NFR-6, NFR-7)
Новые write-хелперы в `plane_sync.py`, все через `_guard_allows_write` (ORCH-117), все never-raise:
- **`set_issue_backlog(work_item)`** — `get_project_states(pid)["backlog"]` → `_set_issue_state_direct`
(ключ `backlog` уже в `_DEFAULT_STATES`). Зеркало `set_issue_done`/`set_issue_in_review`.
- **`set_issue_point(work_item, value:int)`** — PATCH `{"point": int(value)}` (легаси целочисленное поле,
устойчиво — не зависит от estimate-системы). Это запись **факта** (BR-8).
- **`set_issue_estimate_point(work_item, value)`** — резолв estimate-point UUID через новый
`get_project_estimate_points(project_id)` (GET project → `estimate` id → GET estimate-points → map
`value→uuid`, TTL-кэш по образцу `get_project_states`/ORCH-068), затем PATCH `{"estimate_point": <uuid>}`.
Это запись **прогноза** (BR-7).
- **fail-safe (NFR-7):** estimate-система не настроена / значение вне системы / поле отсутствует / 4xx →
**best-effort пропуск + лог**, не падение. `point` устойчивее `estimate_point` (сырой int), но оба
best-effort.
- **Коммент** — `add_comment` с прогнозом (стоимость/время/токены/story points), `author="stream"`.
- Прогноз пишется в `estimate_point`, факт — в `point`; поля **не перепутаны**; факт **не перезаписывает**
`estimate_point` (AC-3/AC-4).
### D7 — Персистентность: аддитивная `task_estimates`, UPSERT по `work_item_id` (BR-10, FR-T9, NFR-8, AC-T4, AC-12)
- **Новая аддитивная таблица** `task_estimates` (`CREATE TABLE IF NOT EXISTS` в `init_db()`, паттерн
`coverage_baseline`/`lessons`/`transition_lease`), **`UNIQUE(work_item_id)`** для идемпотентного UPSERT.
Полная схема, типы, индексы — `08-data-requirements.md`.
- Хелперы `db.record_estimate(**)` (UPSERT прогноза по `work_item_id`), `db.set_actual(work_item_id, ...)`
(запись факта+дельты), `db.get_estimate(work_item_id)`, `db.estimates_snapshot()`.
- Ключ — `work_item_id` (на момент оценки `task_id` может быть `NULL` — issue на бэклоге, строки `tasks`
ещё нет). `task_id` заполняется позже, когда оценённый issue входит в пайплайн (best-effort).
- Существующие таблицы — **не изменяются** (NFR-8).
### D8 — Поверхности отображения: Plane-коммент + пункт «Оценка» в Telegram-карточке (BR-9, FR-T8, AC-5, AC-6)
- **Plane-коммент** — D6.
- **Telegram-карточка** — пункт **«Оценка»** (время · токены · стоимость) в рендере общей карточки
(`notifications.update_task_tracker`), читается из `task_estimates` по `work_item_id`; never-raise; пустой
прогноз → пункт опускается; инвариант «одна карточка на задачу» (ORCH-087) не нарушается;
HTML-data-слоты экранируются `html.escape` ровно один раз (канон ORCH-095).
- **Замечание о времени появления строки:** карточка существует у pipeline-задачи; если оценка сделана на
бэклоге до старта пайплайна — строка «Оценка» появится при первом рендере карточки после старта
(`task_estimates` хранится по `work_item_id`, переживает старт). Приемлемо и задокументировано.
### D9 — Запись факта на `done` (BR-8, FR-T7, AC-4)
- Тонкая **best-effort врезка** `estimator.record_actual_on_done(task_id, repo, work_item_id)` в
`stage_engine.advance_stage` в существующем блоке `if next_stage == "done"` (`src/stage_engine.py:521`),
ПОСЛЕ terminal-sync, в своём `try/except` (never-raise; зеркало release-merge-lease-врезки рядом).
- Считает факт из `usage.task_usage_summary(task_id)` + тайминги → `story_points_for(actual)` →
`db.set_actual(...)` + `set_issue_point(work_item, actual_sp)`. **Не** перезаписывает `estimate_point`.
- `STAGE_TRANSITIONS`/гейт `check_deploy_status`/machine-verdict — не трогаются (врезка после решения о
переходе, не влияет на него).
### D10 — Толерантность к массовости (NFR-5, AC-T3)
- **Сглаживание встроено в выбор D1:** детерминированная эвристика без LLM/субпроцесса → per-issue ядро
O(1) (пара индексированных чтений). Доминирующая стоимость — несколько ограниченных Plane HTTP-раундов на
issue, исполняемых off-loop (`to_thread`).
- **Новой очереди НЕ вводим:** очередь `jobs`/`max_concurrency` — для агентов (control-path); оценка не
занимает её слот (NFR-3). Опциональный простой in-process семафор `estimator_max_inflight` (дефолт
«щедрый», эффективно off) — конфиг-семя на случай измеренной перегрузки; в v1 не активничает.
- Один webhook не гасит остальные (N независимых вызовов).
### D11 — leaf-инварианты, флаги, наблюдаемость (NFR-2, NFR-3, FR-T10, AC-9)
- **Leaf `src/estimator.py`** (never-raise, паттерн `bug_fast_track`/`coverage_gate`): импортирует только
`config` (+ лениво `db`/`usage`/`plane_sync`/`notifications`/`qg.checks`), не импортирует `stage_engine`/
`launcher`. Публичные: `applies(repo)`, `estimate(...)`, `story_points_for(...)`,
`record_actual_on_done(...)`, `snapshot()`.
- **Флаги** (`config.py`, дефолты безопасные): `estimator_enabled` (kill-switch, env
`ORCH_ESTIMATOR_ENABLED`), `estimator_repos` (CSV, env `ORCH_ESTIMATOR_REPOS`; **пусто → self-hosting
only**), + тюнинг `estimator_min_samples`, `estimator_bootstrap_tokens/cost_usd/seconds`,
`estimator_sp_cost_thresholds`, `estimator_wall_cap_s`, `estimator_max_inflight`.
- `applies(repo)` локально и ПЕРВЫМ → выключенный флаг = нулевой сетевой оверхед, нулевая регрессия для
enduro/orchestrator.
- **Наблюдаемость:** read-only блок `estimator` в `GET /queue` (флаг/скоуп + счётчики прогнозов/записей в
Plane/возвратов-в-Backlog/фактов); при невозможности записи в Plane — лог-warning.
### D12 — Опциональные программные эндпоинты (TRZ §4, AC-7)
- `POST /estimate?work_item=<id>`, `POST /estimate/backlog`, `GET /estimate?work_item=<id>` — **то же ядро**
`estimator.estimate(...)`, идемпотентны. Удобство/диагностика, **не** основной триггер. Их отсутствие не
нарушает приёмку. Не преподносить как единственный способ запуска.
## Альтернативы
- **LLM-оценщик (отдельный вызов на задачу) / гибрид** — отвергнуто на этом шаге: нарушает NFR-4
(стоимость самой оценки), NFR-5 (массовость конкурирует за единственный LLM-транспорт), и политику
ORCH-118 (avoidable LLM control/consultation path). Граница `estimate()` оставляет место под будущий
гибрид без переписывания вызывающих.
- **Авто-оценка каждой задачи на `start_pipeline`** — отвергнуто: это модель, которую заказчик **явно
отклонил** (REJECT 2026-06-17). Оценка — операторский on-demand жест.
- **Новый массовый `POST /estimate-batch` как основной путь** — отвергнуто: массовость даёт сам Plane
multi-select (N вебхуков); batch-эндпоинт — лишний код и второй источник истины.
- **Отдельная стадия/ребро `STAGE_TRANSITIONS` для оценки** — отвергнуто: нарушает NFR-1; оценка не есть
переход стадии. Side-механизм по образцу STOP/Confirm Deploy.
- **Бакетизация по токенам вместо стоимости** — рассмотрено: токены модель-независимы, но заказчик мыслит
осью «сколько стоит». Выбрана стоимость с конфигурируемыми порогами (ре-калибруемыми при ORCH-13);
переключение сигнала — локальная правка за чистой функцией.
- **Хранение оценки в `tasks` колонками** — отвергнуто: на момент оценки строки `tasks` может не быть
(бэклог); ключ по `work_item_id` в отдельной таблице корректнее (NFR-8, аддитивность).
## Последствия
- **+** Оператор видит прогноз (стоимость/время/токены/story points) до отправки задачи в работу; массовая
оценка одним multi-select; пере-оценка идемпотентна; фундамент петли калибровки (ORCH-8) заложен (леджер).
- **+** Нулевая нагрузка на LLM-транспорт и нулевая $-стоимость самой оценки; bulk-безопасно; полностью
детерминировано и тестируемо; согласовано с determinization-политикой ORCH-118.
- **+** Control-path/гейты/горячий путь не тронуты; enduro и текущий orchestrator при выключенном флаге /
на доске без статуса — нулевая регрессия.
- **** Точность прогноза на холодном старте ограничена (мало истории) → митигейшн: bootstrap-дефолты +
петля калибровки порогов поверх леджера (BR-10). Пороги story-points — начальные, подлежат калибровке.
- **** Net-new интеграция с Plane-estimate API (`estimate_point` — FK) добавляет инфра-предусловие
(estimate-система с Fibonacci) и хрупкость записи → митигейшн: best-effort/fail-safe (NFR-7), устойчивый
`point` (raw int) для факта, точная спека — `07-infra-requirements.md`.
- ** (масштаб)** Это **аддитивный leaf по устоявшемуся паттерну** (как serial_gate/coverage_gate/lessons),
без новой стадии, без правки существующих таблиц, без смены БД-движка. **`arch:major-change` не требуется.**
- **Откат:** `ORCH_ESTIMATOR_ENABLED=false` → весь модуль инертен (статус «Оценка» не обрабатывается, нет
записей в Plane/карточку/таблицу; конвейер байт-в-байт до ORCH-020). Доп. откат «на уровне доски» — не
создавать статус «Оценка» (fail-closed). Таблица `task_estimates` остаётся (аддитивна, безвредна).
## Ссылки
- BRD: `docs/work-items/ORCH-020/01-brd.md`
- TRZ: `docs/work-items/ORCH-020/02-trz.md`
- Acceptance: `docs/work-items/ORCH-020/03-acceptance-criteria.md`
- Сквозной ADR: `docs/architecture/adr/adr-0054-task-estimation-status-trigger.md`
- Инфра/данные/риски: `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`
- Сверено по коду: `src/plane_sync.py` (`_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES`/`_guard_allows_write`/write-хелперы),
`src/webhooks/plane.py` (`handle_issue_updated`/`handle_stop`), `src/usage.py:834` (`task_usage_summary`),
`src/db.py` (`has_active_job_for_task`/`_ensure_column`/leaf-DDL-паттерн), `src/stage_engine.py:521`
(`next_stage=="done"`), `src/bug_fast_track.py` (`applies`/label-аппарат), `src/qg/checks.py`
(`is_self_hosting_repo`)
- Прецеденты: ORCH-090 (STOP), ORCH-059 (Confirm Deploy), ORCH-117 (write-guard), ORCH-019 (`track`),
ORCH-118 (LLM-политика), ORCH-098 (leaf-таблица), ORCH-087/095 (Telegram-карточка)

View File

@@ -0,0 +1,76 @@
---
work_item: ORCH-020
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 07 — Инфра-требования: ORCH-020 — Оценка задачи, запускаемая статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: architecture
> When-applicable. Топология контейнеров/сети **не меняется**; затрагиваются только
> Plane-конфигурация (новый статус + estimate-система), env-флаги и онбординг-канон.
> Это инфра-предусловия записи/триггера (NFR-7), а не изменение хост-топологии.
## I-1. Топология / окружения
**N/A для контейнеров/портов/сети/томов.** Новых сервисов/контейнеров/портов нет. Модуль `estimator`
работает внутри существующего процесса `orchestrator` (8500); никаких новых демонов/потоков.
**Plane-конфигурация (предусловия, разовые, человек/онбординг):**
- **P-1. Статус «Оценка» на доске проекта ORCH.** Создать board-статус с **точным именем** `Оценка`.
Его отсутствие = fail-closed no-op (BR-T5): `proj_states.get("estimate") is None` → ветка инертна.
- **Группа статуса.** «Оценка» — транзиентный backlog-side статус (issue в нём лишь на время оценки,
затем оркестратор возвращает в `Backlog`). Рекомендуемая Plane-группа — **`backlog`** или
`unstarted` (косметика индикации). **Запрещена** группа `completed`/`cancelled`: терминал-детект
ORCH-068 (по `group`) иначе ложно посчитает оценку терминалом. Это **обязательный** инвариант группы
(зеркало правила ORCH-009: терминальные группы только у Done/Cancelled/STOP).
- **P-2. estimate-система Plane (для `estimate_point`).** Настроить на проекте ORCH estimate-систему типа
**Points** со значениями Fibonacci **`1, 2, 3, 5, 8`** (под `project.estimate`). `estimate_point` — FK
на estimate-point этой системы; запись прогноза резолвит `value → estimate_point UUID`
(`plane_sync.get_project_estimate_points`, TTL-кэш). **Отсутствие/частичная конфигурация → best-effort
пропуск записи `estimate_point` + лог, без падения конвейера** (NFR-7). Поле факта `point`
целочисленное, устойчиво и пишется сырым int независимо от estimate-системы.
## I-2. Переменные окружения / секреты
**Новых секретов/токенов НЕТ** (NFR-6). Запись в Plane идёт существующими `PLANE_HEADERS` под guard
ORCH-117. Новые конфиг-флаги (`config.py`, env-префикс `ORCH_`; дефолты безопасные — пустой `.env` = off
для не-self-hosting, self-hosting-only при пустом скоупе):
| Ключ | env | Дефолт | Назначение |
|------|-----|--------|-----------|
| `estimator_enabled` | `ORCH_ESTIMATOR_ENABLED` | `True` | kill-switch (False → модуль инертен, нулевая регрессия) |
| `estimator_repos` | `ORCH_ESTIMATOR_REPOS` | `""` | CSV-скоуп; **пусто → self-hosting only** (`orchestrator`) |
| `estimator_min_samples` | `ORCH_ESTIMATOR_MIN_SAMPLES` | `3` | порог истории ниже которого включается bootstrap |
| `estimator_bootstrap_tokens` | `ORCH_ESTIMATOR_BOOTSTRAP_TOKENS` | *(реализация)* | дефолт токенов при пустой истории |
| `estimator_bootstrap_cost_usd` | `ORCH_ESTIMATOR_BOOTSTRAP_COST_USD` | *(реализация)* | дефолт стоимости при пустой истории |
| `estimator_bootstrap_seconds` | `ORCH_ESTIMATOR_BOOTSTRAP_SECONDS` | *(реализация)* | дефолт времени при пустой истории |
| `estimator_sp_cost_thresholds` | `ORCH_ESTIMATOR_SP_COST_THRESHOLDS` | `0.50,2.00,5.00,12.00` | пороги бакета story-points (t1,t2,t3,t5), `<=` по возрастанию |
| `estimator_wall_cap_s` | `ORCH_ESTIMATOR_WALL_CAP_S` | *(реализация)* | отсечка аномального wall-времени в истории (зеркало `tracker_brd_review_cap_s`) |
| `estimator_max_inflight` | `ORCH_ESTIMATOR_MAX_INFLIGHT` | *(щедрый/off)* | опц. семафор сглаживания массовой нагрузки (v1 неактивен) |
`.env.example` — добавить блок `ORCH_ESTIMATOR_*` как канон ключей старта (норматив ORCH-101: дефолт =
боевое значение).
## I-3. Деплой / рестарт
- **Прод-рестарт `orchestrator` в рамках задачи — НЕ выполнять** (self-hosting инвариант: общий прод
обслуживает enduro). Изменения вступают штатно: код — через прод-выкат **только** после staging-гейта
(8501) по `docs/operations/INFRA.md`; флаги — через управляемый рестарт оператором по runbook.
- **Plane-предусловия P-1/P-2 настраиваются в Plane UI/API** оператором — вне рантайма, вне деплоя орка.
- **Миграция БД** — аддитивная (`CREATE TABLE IF NOT EXISTS task_estimates` в `init_db()`), применяется
идемпотентно на старте; рестарт прод-контейнера ради неё не нужен (применится при следующем штатном
старте). Детали — `08-data-requirements.md`.
- **Онбординг нового проекта (ORCH-009).** Канон онбординга расширяется: статус «Оценка» становится
**23-м** статусом (`onboard_project.py` импортирует имена из `plane_sync._PLANE_NAME_TO_KEY`
добавление `"Оценка"→"estimate"` автоматически попадает в проверку; группа `backlog`/`unstarted`
фиксируется в каноне групп). estimate-система Fibonacci — добавить как `manual-step`/ensure в
онбординг-runbook (Plane CE API может не покрывать estimate-настройку → честный `manual-step`,
fail-safe). Это **follow-up по онбордингу**, не блокер ORCH-020 (на существующем проекте ORCH
предусловия настраиваются руками).
## I-4. CI/CD
**Без изменений `.gitea/workflows/`.** Новый тест-модуль `tests/test_orch020_estimator.py` исполняется
существующим `pytest tests/` (CI / coverage-gate / merge-gate re-test) штатно — без новых шагов CI.
Новых внешних зависимостей нет (эвристика — stdlib + существующие `httpx`/`db`).

View File

@@ -0,0 +1,85 @@
---
work_item: ORCH-020
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 08 — Требования к данным: ORCH-020 — Оценка задачи, запускаемая статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: architecture
> When-applicable / информационный (гейтом не парсится). Одна **новая аддитивная** таблица; существующие
> таблицы (`tasks`/`agent_runs`/`jobs`/…) — **не изменяются** (NFR-8).
## Изменения схемы БД
**Новая аддитивная таблица `task_estimates`** (`CREATE TABLE IF NOT EXISTS` в `db.init_db()`, паттерн
`coverage_baseline`/`lessons`/`transition_lease`; идемпотентно, restart-safe на общей прод-БД):
```sql
CREATE TABLE IF NOT EXISTS task_estimates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
work_item_id TEXT NOT NULL UNIQUE, -- ключ/UPSERT-цель (issue может не иметь task на момент оценки)
task_id INTEGER, -- FK tasks.id; НУЛЛАБЕЛЕН до старта пайплайна
repo TEXT,
-- Прогноз (на момент перевода в «Оценка»):
forecast_tokens INTEGER,
forecast_seconds INTEGER,
forecast_cost_usd REAL,
forecast_story_points INTEGER, -- из {1,2,3,5,8}
-- Факт (на момент перехода задачи в `done`):
actual_tokens INTEGER,
actual_seconds INTEGER,
actual_cost_usd REAL,
actual_story_points INTEGER, -- из {1,2,3,5,8}
-- Метаданные:
source TEXT, -- 'status' | 'manual' | 'api'
estimate_count INTEGER NOT NULL DEFAULT 1, -- число пере-оценок (инкремент при UPSERT)
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_task_estimates_repo ON task_estimates (repo);
CREATE INDEX IF NOT EXISTS idx_task_estimates_task_id ON task_estimates (task_id);
```
- **`UNIQUE(work_item_id)`** — несущий инвариант идемпотентной пере-оценки (BR-T4/AC-T4): повторный перевод
в «Оценка» делает **UPSERT** (`INSERT … ON CONFLICT(work_item_id) DO UPDATE …`), обновляя одну строку и
инкрементируя `estimate_count`; дублей строк нет.
- **Дельта** прогноз↔факт **не хранится отдельной колонкой** — вычисляется на чтение из forecast/actual
(избегаем рассинхрона; калибровке достаточно обеих величин). При желании реализатор может добавить
материализованные `delta_*` — не обязательно (BR-10 требует «обе величины + дельту»; вычисляемая дельта
это удовлетворяет).
- **Индексы:** по `repo` (выборка/снапшот по проекту) и `task_id` (связь с задачей). По `work_item_id`
индекс создаётся автоматически (UNIQUE).
## Новые/изменённые сущности
- **Хелперы `db.py`** (каждый открывает/закрывает свою connection, паттерн `coverage_baseline`/`lessons`;
leaf `estimator`/вызывающие оборачивают в never-raise):
- `record_estimate(work_item_id, repo, task_id=None, forecast_*=…, source='status') -> int` — UPSERT
прогноза по `work_item_id`; инкремент `estimate_count`, стамп `updated_at`.
- `set_actual(work_item_id, actual_tokens, actual_seconds, actual_cost_usd, actual_story_points,
task_id=None) -> bool` — запись факта; **не трогает** forecast-поля.
- `get_estimate(work_item_id) -> dict | None` — текущая строка прогноз/факт.
- `estimates_snapshot(limit=…) -> dict` — read-only для блока `estimator` в `GET /queue`.
- **Read-only агрегат истории** `db.completed_task_stats(repo, track) -> {n, mean_tokens, mean_cost_usd,
mean_seconds}` — поверх `agent_runs` (токены/стоимость, как `task_usage_summary`) и `tasks`
(`stage='done'`, время = `updated_at created_at` с отсечкой `estimator_wall_cap_s`). **Только чтение**
существующих таблиц; новых колонок не вводит.
## Совместимость данных / миграции
- **Аддитивность (NFR-8):** только новая таблица + новые read/write-хелперы; **ни одной** правки
существующих таблиц/колонок/индексов. `STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict-ключи независимы
от данных оценки.
- **Идемпотентность миграции:** `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` — no-op на уже
созданной таблице; безопасно на живой общей прод-БД (enduro не затронут — таблица общая, но писать в неё
будет только self-hosting-скоуп; строки enduro не появляются, пока репо вне `estimator_repos`).
- **Restart-safe:** строки `task_estimates` переживают рестарт; прогноз, сделанный на бэклоге (с
`task_id=NULL`), сохраняется до старта пайплайна и связывается с `task_id` позже (best-effort).
- **Влияние на общую прод-БД:** таблица малая (одна строка на оценённый issue), индексы лёгкие; нагрузка на
hot-path **нулевая** (claim/queue не читают `task_estimates`). Откат (`ORCH_ESTIMATOR_ENABLED=false`)
оставляет таблицу пустой/неиспользуемой — безвредно.

View File

@@ -0,0 +1,47 @@
---
work_item: ORCH-020
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-020 — Оценка задачи, запускаемая статусом «Оценка»
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Риски реализации и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Массовый перевод (десятки issue → десятки вебхуков) перегружает прод**, тормозя обслуживание enduro | Сред. | Выс. | D1: детерминированная эвристика **без LLM/субпроцесса** (ядро O(1)); off-loop `to_thread`; **не** занимает слот `jobs/max_concurrency`; опц. семафор `estimator_max_inflight` как семя; никакого LLM-fan-out за единственный транспорт |
| TR-2 | **Статус «Оценка» выдёргивает in-flight задачу** в Backlog / прерывает работу | Низ. | Выс. | D5: guard `has_active_job_for_task` → no-op при активном job; авто-возврат только в `Backlog`, никогда не трогает `STAGE_TRANSITIONS`/стадии |
| TR-3 | **Цикл вебхуков** (возврат в Backlog → новый webhook → повторная оценка) | Низ. | Сред. | D4/D5: `backlog` не совпадает ни с одной триггер-веткой → входящий «state→Backlog» = no-op-эхо (анти-loop по построению) |
| TR-4 | **estimate-система Plane не настроена / `estimate_point` — FK** → запись прогноза падает/невозможна | Сред. | Сред. | D6/NFR-7: best-effort пропуск + лог, never-raise; факт пишется в устойчивый int-`point`; точная спека и предусловие — `07-infra-requirements.md` (P-2) |
| TR-5 | **Запись в боевой Plane из теста/worktree** (общая доска) | Низ. | Выс. | D6: все три записи (`estimate_point`/`point`/коммент/состояние) под `_guard_allows_write` (ORCH-117) → из pytest-процесса физически заблокированы |
| TR-6 | **Неточность прогноза на холодном старте** (мало истории) подрывает доверие | Выс. | Низ. | D2: bootstrap-дефолты ниже `estimator_min_samples`; леджер `task_estimates` (D7) + конфигурируемые пороги D3 как фундамент петли калибровки (ORCH-8) |
| TR-7 | **Пороги story-points произвольны** (нет калиброванных данных day-1) | Сред. | Низ. | D3: пороги вынесены в конфиг `estimator_sp_cost_thresholds`; дефолты помечены как bootstrap; ре-калибровка поверх леджера без правки кода |
| TR-8 | **Расползание в Шаг 2** (per-task override модели/эффорта = касание control-path) | Низ. | Выс. | NFR-1/NFR-3/AC-11: жёсткий out-of-scope; горячий путь `resolve_agent_model`/`resolve_agent_effort`/`_spawn` не трогается; follow-up на отдельный work item (зависимость ORCH-13) |
| TR-9 | **Идемпотентность пере-оценки нарушена** (дубли строк) | Низ. | Сред. | D7: `UNIQUE(work_item_id)` + UPSERT; покрыто TC-07 |
| TR-10 | **Дрейф канона онбординга** (23-й статус «Оценка» не учтён) | Низ. | Низ. | D4 + `07` I-3: имя статуса берётся из `_PLANE_NAME_TO_KEY` (онбординг-проверка ловит автоматически); группа фиксирована каноном; estimate-система — `manual-step` |
| TR-11 | **Строка «Оценка» в Telegram-карточке роняет рендер** | Низ. | Сред. | D8: never-raise; пустой прогноз → пункт опускается; `html.escape` один раз (ORCH-095); инвариант «одна карточка» (ORCH-087) не нарушается |
| TR-12 | **Врезка факта на `done` влияет на переход** | Низ. | Выс. | D9: best-effort `try/except` ПОСЛЕ решения о переходе; `check_deploy_status`/`STAGE_TRANSITIONS`/machine-verdict не трогаются |
## Сводный вывод
Доминирующий класс рисков — **операционные** (массовость TR-1, инфра-предусловия Plane TR-4/TR-10) и
**точностные** (TR-6/TR-7), а **не** архитектурно-структурные: control-path, гейты, горячий путь запуска
агентов и схемы существующих таблиц **не затрагиваются** (NFR-1/NFR-3), всё аддитивно, под kill-switch,
never-raise, fail-safe, скоуп self-hosting.
Выбор детерминированной эвристики (D1) **снимает корневой риск масштаба** (TR-1) и согласован с
determinization-политикой ORCH-118 — это сознательно консервативное, обратимое решение.
**Эскалация `arch:major-change` не требуется:** изменение — аддитивный leaf по устоявшемуся паттерну
(serial_gate/coverage_gate/lessons/cancel), без новой стадии, без правки существующих таблиц, без смены
БД-движка. **Возврат в анализ не требуется:** ТЗ реализуемо без нарушения принципов архитектуры.
Остаточный риск для прод-конвейера (self-hosting) — **низкий**; точность прогноза — итеративно улучшаемая
через заложенный леджер калибровки.

View File

@@ -0,0 +1,106 @@
---
verdict: APPROVED
work_item: ORCH-020
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-17
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-020
version: 2
---
# Review ORCH-020 — Оценка задачи, запускаемая Plane-статусом «Оценка»
## Summary
Реализация **технически сильная и полная**: новый leaf `src/estimator.py` (never-raise, kill-switch
`estimator_enabled` + скоуп `estimator_repos`), детерминированная эвристика по истории завершённых
задач **без LLM** (как требует ADR-001 D1 и политика determinization ORCH-118), точечные аддитивные
врезки в `webhooks/plane.py` / `plane_sync.py` / `stage_engine.py` / `db.py` / `notifications.py` /
`main.py` / `config.py`. Все критерии приёмки (AC-T1…AC-T6, AC-1…AC-12) покрыты тестами
`tests/test_orch020_estimator.py` (TC-01…TC-20).
**Прогон тестов (зелёный):**
- `tests/test_orch020_estimator.py` + анти-дрейф доков (`test_system_docs`, `test_bundled_setup_doc`,
`test_lite_setup_doc`) — **92 passed**.
- Регрессы интеграционных точек (webhook/plane/config/stage/notif/db/queue/estimator) — **540 passed**.
**Соответствие ТЗ (ось 1) — полное.** FR-T1…FR-T10 реализованы: статус «Оценка» как fail-closed-триггер
(`_PLANE_NAME_TO_KEY["Оценка"]="estimate"`, отсутствует в `_DEFAULT_STATES`), `handle_estimate`
(guard-цепочка applies→анти-disruption), прогноз 4 величин + story-points `{1,2,3,5,8}`,
авто-возврат в Backlog (анти-loop), массовость (N вебхуков), идемпотентный UPSERT по `work_item_id`,
запись прогноза в `estimate_point` / факта в `point` (не перепутаны, факт не затирает прогноз),
Telegram-пункт + Plane-коммент, леджер `task_estimates`, опциональные `POST/GET /estimate`, read-only
блок `estimator` в `GET /queue`.
**Соответствие ADR (ось 2) — полное.** D1D12 реализованы дословно. Трассировка (ORCH-078): правки
маркированных блоков (`handle_issue_updated` — семейство STOP/Confirm Deploy; `_PLANE_NAME_TO_KEY`;
done-блок `stage_engine` с ORCH-114 lease/merge-lease-release) **аддитивны** и не ломают
зафиксированных инвариантов — ветка `estimate` стоит после `stop`, взаимоисключение жестов держится
различием UUID (проверено TC-02b), done-врезка идёт в своём `try/except` ПОСЛЕ решения о переходе.
**Качество кода (ось 3) — высокое.** never-raise-контракт выдержан во всех публичных функциях и
врезках; leaf не импортирует `stage_engine`/`launcher` на загрузке (TC-20). Сверено по коду:
колонки `agent_runs` (`input_tokens`/`output_tokens`/`cache_read_tokens`/`cache_creation_tokens`),
используемые `completed_task_stats`, существуют; `add_comment(..., author=)` — корректная сигнатура;
базис токенов прогноза (`completed_task_stats`) и факта (`task_usage_summary.total_in+total_out`)
**консистентен** (оба = input+cache_read+cache_creation+output) → дельта прогноз↔факт осмысленна.
**Документация (ось 4, приоритет) — обновлена в том же PR.** `src/` изменён И документация обновлена:
инженерный справочник, ADR (work-item + сквозной adr-0054), CHANGELOG, README (env-таблица), онбординг/
Lite/Bundled-доки + анти-дрейф тесты, **и бизнес-витрина `docs/overview/` (ORCH-011)** — устранён
блокер предыдущего ревью (run_id=800).
**Control-path не тронут (AC-10 подтверждён):** `git diff` не затрагивает `src/stages.py` и
`src/qg/checks.py`; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схемы
существующих таблиц — байт-в-байт; `task_estimates` — единственный аддитивный объект
(`CREATE TABLE/INDEX IF NOT EXISTS`).
> **Контекст вердикта.** Предыдущее ревью (run_id=800) вынесло `REQUEST_CHANGES` c единственным P1 —
> витрина `docs/overview/` не отражала новую операторскую способность «Оценка». Блокер **устранён**
> коммитом `375bd46 docs(overview): отразить операторскую способность «Оценка» в витрине (ORCH-020)`;
> попутно ужесточена тавтологичная строка TC-20 (бывший P3). Оставшихся P0/P1 нет → `APPROVED`.
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет — предыдущий P1 «витрина `docs/overview/` не обновлена» закрыт коммитом `375bd46`; см.
раздел «Документация»)
### P2 — Should fix
- (нет)
### P3 — Nice-to-have
- [ ] `src/plane_sync.py::_patch_issue_fields` — PATCH полей issue (`point`/`estimate_point`) проходит
guard ORCH-117 с меткой `plane_write_guard.OP_STATE`, хотя это не смена state. Функционально
безопасно (решение guard'а зависит от sandbox/prod-проекта, не от op-метки; op влияет лишь на
аудит-лог), но метка семантически неточна. Опционально — ввести/использовать op-константу для
field-PATCH ради точности аудит-лога. **Не блокирует.**
## Документация
**Обновлено в PR (корректно, golden source наравне с кодом):**
- `docs/architecture/README.md` — новый компонент «Task estimator», секция «Оценка задачи (ORCH-020)»,
таблица `task_estimates` в data-model, эндпоинты `POST/GET /estimate`, блок `estimator` в `GET /queue`,
статусная модель.
- ADR: `docs/work-items/ORCH-020/06-adr/ADR-001-task-estimation-status-trigger.md` + сквозной
`docs/architecture/adr/adr-0054-task-estimation-status-trigger.md`.
- `CHANGELOG.md` (`[Unreleased]`), `README.md` (env-таблица `ORCH_ESTIMATOR_*`, 9 ключей).
- `docs/operations/ONBOARDING.md`, `docs/deployment/LITE_SETUP.md`, `docs/deployment/BUNDLED_SETUP.md`
+ анти-дрейф тесты (22→23 статуса, сверка импортом `_PLANE_NAME_TO_KEY`); `scripts/onboard_project.py`
— 23-й статус «Оценка» (группа `unstarted`, не терминальная — корректно, иначе ложно сработал бы
терминал-детект ORCH-068).
- **Бизнес-витрина `docs/overview/` (ORCH-011) — обновлена в этом же PR (закрытие P1 прошлого ревью):**
`business.md` (способность «Оценка задачи до запуска» + сценарий 7), `presentation.md`
(исправлено «управляющих статусов ровно три» → жесты + оценка; добавлены жест и сценарий),
`tech-pipeline.md` (новая секция «Оценка задачи» + исправление статусной модели), `tech-integrations.md`
(исправлено «три управляющих статуса»), `tech-observability.md` (пункт «Оценка» в карточке + блок
`estimator` в `GET /queue`), `tech-data-model.md` (таблица `task_estimates`). Машинные факты витрины
(`tests/test_system_docs.py`) — зелёные.
**Требует обновления:** нет (документация полна и консистентна реализации).

View File

@@ -0,0 +1,41 @@
---
result: PASS
work_item: ORCH-020
stage: testing
author_agent: test-runner
status: success
created_at: 2026-06-17
model_used: n/a
exit_code: 0
smoke: ok
---
# Test Gate Log (deterministic runner, ORCH-116)
pytest exit-code `0` -> `result: PASS` (smoke: ok).
Вердикт зафиксирован детерминированным test-раннером (ORCH-116), не LLM. PASS/FAIL = exit-код `pytest` + read-only smoke (`/health`, `/status`, `/queue` + блок `serial_gate`).
pytest stdout (tail):
```
%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 76%]
........................................................................ [ 80%]
........................................................................ [ 83%]
........................................................................ [ 86%]
........................................................................ [ 89%]
........................................................................ [ 92%]
........................................................................ [ 96%]
........................................................................ [ 99%]
................ [100%]
=============================== warnings summary ===============================
src/config.py:8
/repos/_wt/orchestrator/feature_ORCH-020-/src/config.py:8: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
class Settings(BaseSettings):
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
2248 passed, 1 warning in 116.06s (0:01:56)
```

View File

@@ -0,0 +1,46 @@
---
staging_status: SUCCESS
work_item: ORCH-020
stage: deploy-staging
author_agent: staging-runner
status: success
created_at: 2026-06-17
model_used: n/a
exit_code: 0
base_url: http://localhost:8501
---
# Staging Gate Log (deterministic runner, ORCH-115)
Staging suite exit-code `0` -> `staging_status: SUCCESS`.
Вердикт зафиксирован детерминированным staging-раннером (ORCH-115), не LLM. infra-tolerance (ORCH-061) уже учтена внутри `staging_check.py` — раннер её не пересуживает.
INFRA-WAIVED lines (ORCH-061, copied for observability):
- INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
Staging suite stdout (tail):
```
(waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
✗ FAIL C9b Analyst job enqueued in staging queue
[CLEANUP]
· CLEANUP: no branch to delete
✓ PASS CLEANUP: deleted Plane issue df2396e6-fec8-403a-9fa5-9587d0c8465d (HTTP 204)
· CLEANUP DB: no task row found for plane_id=df2396e6-fec8-403a-9fa5-9587d0c8465d
· CLEANUP DB dedup: no such table: events_dedup
============================================================
 RESULT: 8/10 checks PASS
REAL failed : none
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
============================================================
· tolerance: staging_infra_tolerance_enabled=True
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```

View File

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

View File

@@ -0,0 +1,22 @@
---
coverage_status: FAIL
work_item: ORCH-020
measured_coverage: 79.48
baseline: 80.45
floor: 0.00
policy: both
epsilon: 0.50
delta: -0.97
---
# Coverage Report — ORCH-020
Детерминированный гейт покрытия (ORCH-027) — под-гейт ребра `deploy-staging→deploy` (ПОСЛЕ merge-gate, ДО image-freshness). Машинный вердикт читается ТОЛЬКО из `coverage_status:` frontmatter выше.
## Verdict
measured=79.48% policy=both eps=0.50: absolute 79.48% >= floor 0.00%-eps0.50 -> PASS; baseline 79.48% >= base 80.45%-eps0.50 -> FAIL
## Measurement
pytest --cov=src: line coverage src/ = 79.48%
## Policy
policy=both, floor=0.0%, baseline=80.45%, epsilon=0.5%

View File

@@ -0,0 +1,7 @@
# Business Request: FAQ: как использовать STOP для отмены задачи
Work Item ID: ORCH-108
## Description
_(описание отсутствует в источнике)_

View File

@@ -0,0 +1,181 @@
---
work_item: ORCH-108
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 01 — BRD (бизнес-требования): ORCH-108 — FAQ: как использовать STOP для отмены задачи
Work Item: **ORCH-108** · Repo: **orchestrator** (self-hosting) · Стадия: analysis
Тип: документация (пользовательский FAQ). Объём — **только аналитик** (docs-only, без правки `src/**`).
---
## 1. Бизнес-контекст и проблема
Механизм отмены задачи через выделенный Plane-статус **STOP** реализован (ORCH-090,
`docs/architecture/adr/adr-0026-stop-cancel-task.md`): оператор переводит задачу в статус STOP, и
оркестратор останавливает агента, снимает job'ы с очереди, прибирает ветку/worktree и переводит
задачу в терминальное состояние `cancelled`. **Но пользовательской документации «как этим
пользоваться» нет.** Упоминания STOP разрознены и адресованы разным читателям:
- `docs/overview/business.md` — «Сценарий 6: остановить задачу» (витрина, 1 абзац);
- `docs/overview/tech-pipeline.md` — «Отмена: STOP → `cancelled`» (инженерный обзор);
- ADR ORCH-090 — глубокое архитектурное обоснование (не для конечного пользователя).
Пользователь доски Plane (тот, кто заводит/ведёт задачи) не имеет **единой пошаговой инструкции**:
что именно делает STOP, что происходит с веткой/статусом/уведомлениями, что будет, если нажать STOP
во время деплоя, откатывается ли уже влитый в `main` код, и как перезапустить отменённую задачу.
Из-за этого вероятны ошибочные ожидания (например: «STOP откатит мой код из прода» — **неверно**) и
лишние обращения к оператору.
**Боль/риск, который закрываем:** отсутствие самодостаточного FAQ → неверная ментальная модель STOP
→ ошибочные действия на проде (self-hosting: один инстанс обслуживает все проекты) и нагрузка на
оператора.
**Установленные факты (сверено по коду, не изобретать):**
- Маршрут STOP: `src/webhooks/plane.py::handle_issue_updated` распознаёт логический ключ `stop`
(fail-closed: на доске без статуса STOP ветка не активируется) → `handle_stop`
`src/stage_engine.py::cancel_task`.
- `cancel_task` (сверено, `src/stage_engine.py:2443`):
1. **Идемпотентно** — отсутствующая или уже терминальная (`done`/`cancelled`) задача → no-op.
2. **Критичное окно** (`src/cancel.py::in_critical_window`) — задача в необратимой фазе
merge/deploy → **отложенная отмена** (`cancel_requested_at`, снимаются только `queued`-job'ы,
алерт «⏸️ … отложена»; finalizer применяет отмену после честного завершения шага). STOP
**никогда** не трогает `main`, не делает force-push, не рестартит прод-контейнер.
3. **Полный сброс** (вне критичного окна) — SIGTERM агента (graceful-каскад), все job'ы →
терминальный `cancelled`, очистка deploy-state + освобождение merge-lease, снятие worktree,
удаление **рабочей** Gitea-ветки (**не** `main`, без force-push), тумбстон натуральных ключей +
`stage='cancelled'`. **Docs-артефакты сохраняются.**
4. **Наблюдаемость** — Telegram «🛑 … задача ОТМЕНЕНА (STOP)» + Plane-комментарий + обновление
карточки.
- **Перезапуск с нуля** — только «To Analyse» (тумбстон ключей → `get_task_by_plane_id` вернёт
`None` → создаётся свежая задача от актуального `origin/main`). Релонч середины пайплайна закрыт:
«To Analyse» на существующей не-`analysis` задаче → no-op + подсказка «STOP → To Analyse».
- **Простой на `deploy` в ожидании `Confirm Deploy`** (lease держится, но актор не бежит) — **не**
критичен → немедленный полный сброс (ORCH-090 review P1).
- Конфиг: `stop_status_enabled` (kill-switch), `stop_status_repos` (CSV; пусто → все репо). При
выключенном флаге / отсутствии статуса STOP — fail-safe no-op.
- Наблюдаемость для оператора: read-only блок `stop` в `GET /queue` (`src/cancel.py::snapshot`):
`enabled`/`repos`/счётчик `cancelled`/`deferred_pending`/последние отмены.
> **Уточнение к формулировке бизнес-запроса.** В описании сказано: «орк запускает cancel_request,
> откат, затем cancelled». Здесь «откат» = **сброс прогресса задачи** (снятие job'ов, удаление
> рабочей ветки/worktree, возврат задачи в терминал `cancelled`), а **НЕ** git-revert уже влитого в
> `main` кода. `cancel_request` — это путь **отложенной** отмены в критичном окне
> (`cancel_requested_at`), он срабатывает **не всегда**, а только если STOP пришёл во время
> необратимого шага. FAQ обязан развести эти понятия явно (см. BR-4, BR-5).
---
## 2. Объём (scope)
### В объёме
- Создать **пользовательский FAQ** о статусе STOP — единый, самодостаточный, пошаговый документ для
пользователя доски Plane.
- FAQ покрывает: назначение STOP; как отменить задачу; что происходит пошагово (агент, job'ы,
ветка/worktree, статус, уведомления); поведение в критичном окне merge/deploy (отложенная отмена);
явный ответ «STOP не откатывает влитый в `main` код»; как перезапустить отменённую задачу
(«To Analyse»); идемпотентность повторного STOP; что делать, если STOP «не сработал»
(инфра-предусловие — статус STOP на доске, kill-switch); где увидеть результат (Telegram /
Plane-комментарий / `GET /queue`).
- Перекрёстные ссылки между новым FAQ и существующими упоминаниями STOP (витрина / инженерный
обзор), без дублирования источника истины.
### Вне объёма
- Любые изменения кода `src/**`, поведения STOP, `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схемы БД.
Это **docs-only** задача; функциональность STOP уже реализована (ORCH-090) и не меняется.
- Изменение архитектуры/механики отмены, добавление новых статусов/эндпоинтов.
- Перевод FAQ на другие языки, видео/скриншоты-гайды.
- Документирование смежных гейтов (Confirm Deploy / Approved) сверх ссылки-разграничения «STOP ≠
Confirm Deploy».
---
## 3. Заинтересованные стороны
- **Заказчик:** владелец продукта (нужен понятный пользовательский FAQ по STOP).
- **Затрагивает:** пользователей доски Plane (заводят/ведут/отменяют задачи), оператора
(меньше обращений), будущих внешних операторов Lite/Bundled-тиража.
- **Принимает результат:** reviewer (стадия `review`) — проверяет наличие, полноту и фактическую
корректность FAQ против кода.
---
## 4. Бизнес-требования (BR)
- **BR-1 — Единый пользовательский FAQ.** Существует один самодостаточный документ-FAQ о статусе
STOP, написанный для пользователя доски Plane (не для разработчика), в формате «вопрос → ответ».
- **BR-2 — Пошаговая инструкция отмены.** FAQ объясняет, как отменить задачу (перевести issue в
статус STOP на доске) и что для этого нужно (статус STOP должен существовать на доске).
- **BR-3 — Что происходит при STOP.** FAQ описывает наблюдаемые пользователем последствия: агент
останавливается, job'ы снимаются, рабочая ветка/worktree удаляются, задача переходит в
`cancelled`, приходит уведомление в Telegram и комментарий в Plane; **docs-артефакты задачи
сохраняются**.
- **BR-4 — Отложенная отмена в критичном окне.** FAQ объясняет: если STOP нажат во время
необратимого шага (слияние/выкладка), отмена **откладывается** до честного завершения шага;
`main`/прод при этом не трогаются.
- **BR-5 — STOP ≠ откат прод-кода.** FAQ содержит **явный** ответ: STOP сбрасывает незавершённый
прогресс задачи, но **не откатывает** код, уже влитый в `main`/прод (revert — отдельная задача).
- **BR-6 — Перезапуск отменённой задачи.** FAQ объясняет: отменённую задачу нельзя «продолжить с
середины»; перезапуск — только «To Analyse», который создаёт задачу **с нуля** (новая ветка от
актуального `main`).
- **BR-7 — Идемпотентность и «не сработало».** FAQ объясняет: повторный STOP по уже отменённой/
завершённой задаче безопасен (no-op); если STOP «ничего не сделал» — вероятные причины (статус
STOP не заведён на доске / задача уже терминальна / отмена отключена для репо).
- **BR-8 — Где увидеть результат.** FAQ указывает источники подтверждения отмены: карточка
Telegram, комментарий в Plane, read-only блок `stop` в `GET /queue`.
- **BR-9 — Согласованность с витриной.** FAQ не противоречит существующим упоминаниям STOP в
`docs/overview/business.md` и `docs/overview/tech-pipeline.md`; ссылки связывают их без
дублирования источника истины.
---
## 5. Нефункциональные требования (NFR)
- **NFR-1 — Docs-only, нулевой рантайм-риск.** Никаких изменений `src/**`, конвейера, гейтов, схемы
БД. Self-hosting-безопасно: задача не деплоит/не рестартит прод/не трогает `main`.
- **NFR-2 — Фактическая точность.** Каждое утверждение FAQ verifiable против кода (`src/cancel.py`,
`src/stage_engine.py::cancel_task`, `src/webhooks/plane.py`, `src/config.py`). Запрещены неверные
обещания (например «STOP откатит прод»).
- **NFR-3 — Язык и аудитория.** Русский, тон — пользовательский (без требования читать код/ADR);
термины пайплайна поясняются простыми словами.
- **NFR-4 — Сопровождаемость / анти-дрейф.** Структуру FAQ закрывает детерминированный структурный
тест (без сети/LLM/subprocess), по образцу `tests/test_lite_setup_doc.py`, чтобы будущие правки не
«отклеивали» FAQ от фактов.
- **NFR-5 — Без форка источника истины.** FAQ ссылается на канон (ADR ORCH-090, инженерный обзор), а
не копирует его дословно; машинные детали — ссылками.
---
## 6. Допущения и ограничения
- **Допущение A1 (размещение).** FAQ размещается как новый документ `docs/operations/FAQ_STOP.md`
(раздел эксплуатации/операторских runbook'ов — там же `ONBOARDING.md`, `PHANTOM_MERGE_RUNBOOK.md`).
Это **разумный дефолт** исходя из аудитории «оператор/пользователь доски»; точное имя/раздел
reviewer/architect может скорректировать, но это не блокирует анализ (не сигнальный вопрос).
- **Допущение A2 (язык).** Русский — основной язык пользовательской документации проекта
(соответствует `docs/overview/*`).
- **Ограничение C1.** Поведение STOP фиксировано ORCH-090; FAQ его **документирует**, а не меняет.
Если по ходу обнаружится расхождение «доки vs код» — это дефект, заводится отдельно (правило
агентов №4: не комментировать ТЗ задним числом, возвращать в анализ).
- **Ограничение C2.** Никаких блокирующих неоднозначностей не выявлено → файл `01-questions.md`
**не создаётся** (ORCH-120): сделанных допущений (A1/A2) достаточно для корректного пакета.
---
## 7. Критерии успеха
Документ-FAQ создан, покрывает все темы BR-1…BR-9, фактически согласован с кодом, перекрёстно связан
с витриной, и закрыт структурным анти-дрейф тестом. Полный регресс `tests/` остаётся зелёным.
Детальные PASS/FAIL — в `03-acceptance-criteria.md`.
## 8. Риски
- **R1 — Дрейф «доки ↔ код».** Будущая правка STOP сделает FAQ неверным. Митигейшн — структурный
тест (NFR-4) + правило «правишь STOP → обнови FAQ в том же PR».
- **R2 — Ошибочное размещение/дубль.** FAQ продублирует витрину вместо ссылки. Митигейшн — BR-9 +
AC на перекрёстные ссылки.
- Детали/полный перечень — `10-tech-risks.md` (заполняет архитектор; для docs-only задачи риски
минимальны).

View File

@@ -0,0 +1,189 @@
---
work_item: ORCH-108
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-108 — FAQ: как использовать STOP для отмены задачи
Work Item: **ORCH-108** · Repo: **orchestrator** (self-hosting) · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Это **docs-only** задача: код `src/**` и поведение STOP не меняются. Источник истины поведения —
> ORCH-090 (`adr-0026`); здесь — требования к **документации** этого поведения.
> Архитектурное обоснование (если потребуется) — задача архитектора (`06-adr`).
## 1. Сводка изменения
Создать пользовательский **FAQ по статусу STOP** — новый Markdown-документ
`docs/operations/FAQ_STOP.md` в формате «вопрос → ответ», для пользователя доски Plane. Добавить
перекрёстные ссылки из существующих упоминаний STOP (витрина / инженерный обзор) на FAQ. Закрыть
структуру FAQ детерминированным анти-дрейф тестом. **Никаких изменений `src/**`, конвейера, гейтов,
схемы БД, API.** Полный черновик содержания FAQ — в Приложении A (готов к переносу разработчиком;
объём «только аналитик» → существенное наполнение сделано на стадии анализа).
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `docs/operations/FAQ_STOP.md` | **создать** — пользовательский FAQ по STOP (основной deliverable; содержание — Приложение A) |
| `docs/overview/business.md` | изменить — добавить ссылку «Подробнее: FAQ по STOP» в «Сценарий 6: остановить задачу» |
| `docs/overview/tech-pipeline.md` | изменить — добавить ссылку на FAQ в раздел «Отмена: STOP → `cancelled`» |
| `CHANGELOG.md` | изменить — запись `docs: ORCH-108 FAQ по статусу STOP` |
| `tests/test_faq_stop_doc.py` | **создать** — структурный анти-дрейф тест FAQ (образец `tests/test_lite_setup_doc.py`) |
**Описываемые (read-only) модули — FAQ их излагает, НЕ меняет** (для верификации фактов reviewer'ом):
- `src/webhooks/plane.py` — `handle_issue_updated` (распознавание ключа `stop`, fail-closed),
`handle_stop` (делегирование в `cancel_task`), `handle_status_start` (гейт релонча: «To Analyse»
перезапускает только с нуля, не середину пайплайна).
- `src/stage_engine.py::cancel_task` — оркестрация отмены (идемпотентность / критичное окно /
полный сброс / наблюдаемость).
- `src/cancel.py` — `applies` (kill-switch + repo-scope), `in_critical_window` (классификация
необратимого окна), `snapshot` (блок `stop` в `GET /queue`).
- `src/config.py` — `stop_status_enabled` (env `ORCH_STOP_STATUS_ENABLED`), `stop_status_repos`
(env `ORCH_STOP_STATUS_REPOS`, CSV; пусто → все репо).
- `src/main.py` — read-only блок `stop` в `GET /queue`.
## 3. Функциональные требования
### FR-1 — Документ FAQ существует и адресован пользователю (BR-1)
Создать `docs/operations/FAQ_STOP.md`: H1-заголовок про STOP, вводный абзац «для кого/зачем», далее
секции «вопрос → ответ». Тон — пользовательский (без требования читать код). Язык — русский.
### FR-2 — Обязательные секции FAQ (BR-2…BR-8)
FAQ содержит как минимум следующие тематические секции (заголовки — стабильные якоря для теста
NFR-4 / TC-02), каждая отвечает на свой вопрос:
1. **«Что делает статус STOP?»** — назначение: отмена + сброс прогресса задачи.
2. **«Как отменить задачу?»** — перевести issue в статус **STOP** на доске Plane; предусловие —
статус STOP заведён на доске.
3. **«Что происходит, когда я нажимаю STOP?»** — пошагово: агент останавливается → job'ы снимаются
→ рабочая ветка и worktree удаляются → задача переходит в `cancelled` → приходит уведомление в
Telegram и комментарий в Plane. **Docs-артефакты задачи сохраняются.**
4. **«Что если задача в этот момент сливается или деплоится?»** — отложенная отмена: отмена
откладывается до честного завершения необратимого шага; `main`/прод не трогаются.
5. **«Откатит ли STOP уже выложенный код?»** — **Нет.** STOP сбрасывает незавершённый прогресс
задачи, но не делает git-revert уже влитого в `main`/прод кода (это отдельная задача).
6. **«Как перезапустить отменённую задачу?»** — только через «To Analyse»; задача создаётся **с
нуля** (новая ветка от актуального `main`); «продолжить с середины» нельзя.
7. **«Я нажал STOP, но ничего не произошло — почему?»** — вероятные причины: статус STOP не заведён
на доске (fail-closed no-op); задача уже завершена/отменена (идемпотентный no-op); отмена
отключена для репозитория (`stop_status_enabled`/`stop_status_repos`).
8. **«Где увидеть, что задача отменена?»** — карточка Telegram («🛑 … ОТМЕНЕНА (STOP)»), комментарий
в Plane, read-only блок `stop` в `GET /queue`.
### FR-3 — Разграничение STOP ↔ другие управляющие статусы (BR-9)
FAQ кратко разграничивает STOP и человеческие гейты `Approved`/`Confirm Deploy` (STOP — отмена, не
одобрение/деплой), ссылкой на инженерный обзор, без переписывания их семантики.
### FR-4 — Перекрёстные ссылки без дублирования (BR-9, NFR-5)
- В `docs/overview/business.md` («Сценарий 6») и `docs/overview/tech-pipeline.md` («Отмена: STOP →
`cancelled`») добавить ссылку на `FAQ_STOP.md`.
- В FAQ — обратные ссылки на инженерный обзор и ADR ORCH-090 как на источник истины поведения.
- **Не дублировать** машинные детали (маркеры/lease/тумбстон) — давать ссылками.
### FR-5 — Фактическая корректность (NFR-2)
Каждое утверждение FAQ соответствует коду на момент написания (см. §2 read-only модули). Запрещены
утверждения, противоречащие коду — в частности: «STOP откатывает прод», «STOP трогает `main`/делает
force-push», «отменённую задачу можно продолжить с середины», «STOP мгновенно убивает идущий
деплой».
### FR-6 — Анти-дрейф тест (NFR-4)
Создать `tests/test_faq_stop_doc.py` (детерминированный, без сети/LLM/subprocess; только парсинг
файла): FAQ существует; присутствуют все обязательные секции-якоря (FR-2); присутствуют ключевые
факты-«кирпичи» (STOP, `cancelled`, «To Analyse», «main … не», отложенная/deferred); присутствуют
перекрёстные ссылки (FR-4); отсутствуют запрещённые неверные утверждения (FR-5, негативный скан).
## 4. Изменения API
Нет. (FAQ лишь упоминает существующий read-only `GET /queue` блок `stop`.)
## 5. Изменения схемы БД
Нет.
## 6. Требования к новым/изменённым QG checks
Нет. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict — не затрагиваются.
Замечание по coverage-гейту (ORCH-027): docs-only изменение не добавляет строк `src/` → базовая
линия покрытия не меняется; новый `tests/test_faq_stop_doc.py` не покрывает `src/` (структурный
тест документа) и на метрику не влияет.
## 7. Совместимость / регресс
- **Обратная совместимость — полная.** Только добавление/правка docs + новый структурный тест.
Поведение рантайма байт-в-байт прежнее; kill-switch не требуется (нет исполняемого кода).
- **Self-hosting-безопасно.** Не деплоит/не рестартит прод/не трогает `main`; реальный прод-деплой
этой задачи безопасен (docs).
- **Регресс.** Полный `tests/` остаётся зелёным; новый тест читает только файл FAQ.
- **Сопровождение (норматив).** Правишь поведение STOP (`src/cancel.py`/`cancel_task`/маршрут
`stop`) → обнови `docs/operations/FAQ_STOP.md` в том же PR (правило агентов №2 / №6: reviewer
требует обновлённую доку).
---
## Приложение A — Черновик содержания FAQ (готов к переносу в `docs/operations/FAQ_STOP.md`)
> Нормативный ориентир содержания (объём «только аналитик»). Разработчик переносит как тело
> документа; точные формулировки можно полировать, фактическую часть менять нельзя без возврата в
> анализ (правило №4).
```markdown
# FAQ: отмена задачи через статус STOP
Эта страница — для пользователя доски Plane. Она объясняет, что делает статус **STOP**, как им
безопасно остановить задачу и чего от него ждать. Технические детали механизма — в
[инженерном обзоре](../overview/tech-pipeline.md#отмена-stop--cancelled) и
[ADR ORCH-090](../work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md).
## Что делает статус STOP?
STOP — это «кнопка отмены» задачи. Перевод задачи в статус STOP останавливает работу агента, снимает
задачу с очереди, прибирает рабочие материалы (ветку и worktree) и помечает задачу отменённой
(`cancelled`). Безопасно нажимать даже посреди конвейера.
## Как отменить задачу?
Переведите issue в статус **STOP** на доске Plane — так же, как меняете любой другой статус.
Предусловие: на доске должен быть заведён статус **STOP** (группа `cancelled`). Если его нет, STOP
не сработает (см. «ничего не произошло»).
## Что происходит, когда я нажимаю STOP?
По шагам:
1. Активный агент останавливается (мягкая остановка процесса).
2. Все задачи в очереди по этой задаче снимаются и больше не перезапускаются.
3. Рабочая ветка задачи и её worktree удаляются. **Ветка `main` и прод никогда не трогаются.**
4. Задача переходит в терминальное состояние `cancelled`.
5. Приходит уведомление в Telegram («🛑 … задача ОТМЕНЕНА (STOP)») и комментарий в Plane.
**Документы задачи (анализ, ТЗ и т.д.) сохраняются** — удаляются только рабочая ветка и worktree.
## Что если задача в этот момент сливается или деплоится?
Если STOP пришёл во время необратимого шага (слияние в `main` или выкладка), отмена **аккуратно
откладывается** до честного завершения этого шага. Вы увидите уведомление «⏸️ … отмена отложена».
`main` и прод при этом не трогаются; после завершения шага отмена применяется автоматически.
## Откатит ли STOP уже выложенный код?
**Нет.** STOP сбрасывает **незавершённый прогресс** задачи (ветку/worktree/очередь), но **не
откатывает** код, который уже влит в `main` или выложен в прод. Откат выложенного — это отдельная
задача (revert), STOP её не делает.
## Как перезапустить отменённую задачу?
Отменённую задачу нельзя «продолжить с середины». Чтобы начать заново, переведите её в статус
**«To Analyse»** — задача будет создана **с нуля** (новая ветка от актуального `main`, новый анализ).
## Я нажал STOP, но ничего не произошло — почему?
Вероятные причины:
- На доске **нет статуса STOP** — переход не распознаётся (безопасный no-op). Заведите статус STOP
(группа `cancelled`).
- Задача **уже завершена или уже отменена** — повторный STOP ничего не меняет (это нормально).
- Отмена **отключена для репозитория** настройкой (`stop_status_enabled` / `stop_status_repos`) —
обратитесь к оператору.
## Где увидеть, что задача отменена?
- Карточка задачи в **Telegram** покажет «🛑 … ОТМЕНЕНА (STOP)».
- В **Plane** появится комментарий об отмене.
- Оператор может увидеть отмену в служебной странице состояния `GET /queue` (блок `stop`).
## STOP, Approved и Confirm Deploy — в чём разница?
- **STOP** — отменить задачу.
- **Approved** — одобрить артефакт анализа (двигает задачу дальше), деплой не запускает.
- **Confirm Deploy** — подтвердить прод-выкладку.
Подробнее об управляющих статусах — в [инженерном обзоре](../overview/tech-pipeline.md).
```

View File

@@ -0,0 +1,141 @@
---
work_item: ORCH-108
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-108 — FAQ: как использовать STOP
Work Item: **ORCH-108** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
считается провалом). Reviewer проверяет их буквально по файлам репозитория.
---
## AC-1 — FAQ-документ существует и адресован пользователю
**Условие:** создан `docs/operations/FAQ_STOP.md` в формате «вопрос → ответ» для пользователя Plane.
- **PASS:** файл существует; есть H1 про STOP и вводный абзац «для кого/зачем»; тон
пользовательский (не требует чтения кода); язык русский.
- **FAIL:** файла нет; либо это разработческий/архитектурный текст, а не пользовательский FAQ; либо
нет формата «вопрос → ответ».
---
## AC-2 — Покрыты все обязательные темы
**Условие:** FAQ содержит секции, отвечающие на 8 обязательных вопросов TRZ §FR-2.
- **PASS:** присутствуют все темы — (1) что делает STOP; (2) как отменить; (3) что происходит
пошагово; (4) отложенная отмена в критичном окне; (5) STOP не откатывает прод-код; (6) перезапуск
через «To Analyse» с нуля; (7) «ничего не произошло — почему»; (8) где увидеть результат.
- **FAIL:** отсутствует хотя бы одна из тем (1)(8).
---
## AC-3 — Пошаговые последствия STOP описаны верно
**Условие:** тема (3) перечисляет наблюдаемые последствия согласно `cancel_task`.
- **PASS:** перечислены — остановка агента; снятие job'ов; удаление рабочей ветки и worktree; явное
«`main`/прод не трогаются»; переход в `cancelled`; уведомление Telegram + комментарий Plane; явное
«docs-артефакты сохраняются».
- **FAIL:** пропущен или искажён любой из этих пунктов (например утверждается, что удаляются docs,
или что трогается `main`).
---
## AC-4 — Отложенная отмена в критичном окне
**Условие:** тема (4) корректно описывает поведение при STOP во время merge/deploy.
- **PASS:** сказано, что отмена **откладывается** до честного завершения необратимого шага; что
`main`/прод не трогаются; что после завершения шага отмена применяется.
- **FAIL:** утверждается мгновенное прерывание деплоя/слияния, либо что STOP убивает идущий
необратимый шаг, либо тема отсутствует.
---
## AC-5 — STOP ≠ откат прод-кода (явный ответ)
**Условие:** тема (5) явно разводит «сброс прогресса» и «revert выложенного».
- **PASS:** есть явное «Нет»: STOP **не откатывает** код, уже влитый в `main`/прод; revert — отдельная
задача.
- **FAIL:** FAQ обещает/намекает, что STOP откатит прод-код, либо тема отсутствует.
---
## AC-6 — Перезапуск отменённой задачи
**Условие:** тема (6) описывает перезапуск.
- **PASS:** сказано, что перезапуск — только «To Analyse»; задача создаётся **с нуля** (новая ветка
от актуального `main`); «продолжить с середины» нельзя.
- **FAIL:** утверждается возможность продолжить отменённую задачу с середины, либо неверный
механизм перезапуска, либо тема отсутствует.
---
## AC-7 — «Не сработало» и идемпотентность
**Условие:** тема (7) перечисляет причины no-op.
- **PASS:** перечислены — статус STOP не заведён на доске (fail-closed); задача уже терминальна
(идемпотентный no-op); отмена отключена для репо (`stop_status_enabled`/`stop_status_repos`).
- **FAIL:** причины не описаны или описаны неверно (например, утверждается, что повторный STOP
ломает задачу).
---
## AC-8 — Перекрёстные ссылки без дублирования
**Условие:** FAQ связан с витриной/обзором двусторонними ссылками (TRZ §FR-4).
- **PASS:** `docs/overview/business.md` («Сценарий 6») и `docs/overview/tech-pipeline.md` («Отмена:
STOP → `cancelled`») содержат ссылку на `FAQ_STOP.md`; FAQ ссылается на инженерный обзор и ADR
ORCH-090 как на источник истины; машинные детали не дублируются, а даются ссылками.
- **FAIL:** ссылок нет (FAQ-«сирота»); либо FAQ дословно копирует ADR/обзор вместо ссылки.
---
## AC-9 — Фактическая корректность (нет ложных утверждений)
**Условие:** утверждения FAQ соответствуют коду (`src/cancel.py`, `src/stage_engine.py::cancel_task`,
`src/webhooks/plane.py`, `src/config.py`); запрещённых неверных утверждений нет.
- **PASS:** в FAQ отсутствуют утверждения «STOP трогает `main`/делает force-push», «STOP откатывает
прод», «отменённую задачу можно продолжить с середины», «STOP мгновенно убивает идущий деплой».
- **FAIL:** присутствует хотя бы одно противоречащее коду утверждение.
---
## AC-10 — Docs-only, нулевой рантайм-регресс
**Условие:** изменения ограничены документацией + структурным тестом.
- **PASS:** `git diff` не затрагивает `src/**`, `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схему БД;
изменены только `docs/**`, `CHANGELOG.md`, `tests/test_faq_stop_doc.py`; полный `tests/` зелёный.
- **FAIL:** затронут `src/**` или поведение гейтов/конвейера; либо регресс `tests/` красный.
---
## AC-11 — Анти-дрейф тест присутствует и зелёный
**Условие:** структурную целостность FAQ закрывает детерминированный тест (TRZ §FR-6).
- **PASS:** `tests/test_faq_stop_doc.py` существует; проверяет наличие файла, обязательных
секций-якорей, ключевых фактов-«кирпичей», перекрёстных ссылок и отсутствие запрещённых
утверждений; не делает сети/LLM/subprocess; проходит зелёным.
- **FAIL:** теста нет; либо он не детерминирован (сеть/LLM/subprocess); либо красный.
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1 |
| AC-2 | BR-2…BR-8 / FR-2 |
| AC-3 | BR-3 / FR-2(3) |
| AC-4 | BR-4 / FR-2(4) |
| AC-5 | BR-5 / FR-2(5) |
| AC-6 | BR-6 / FR-2(6) |
| AC-7 | BR-7 / FR-2(7) |
| AC-8 | BR-9 / FR-3, FR-4 |
| AC-9 | NFR-2 / FR-5 |
| AC-10 | NFR-1 / FR (docs-only), §7 |
| AC-11 | NFR-4 / FR-6 |

View File

@@ -0,0 +1,74 @@
work_item: ORCH-108
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-17
model_used: claude-opus-4-8
title: "Анти-дрейф структурного FAQ по статусу STOP (docs-only)"
framework: pytest
scope: >
Покрывается СТРУКТУРНАЯ целостность и фактическая непротиворечивость нового
пользовательского документа docs/operations/FAQ_STOP.md (детерминированно: только
парсинг файлов, без сети/LLM/subprocess; образец tests/test_lite_setup_doc.py).
Вне покрытия: поведение STOP в рантайме — оно реализовано и протестировано в
ORCH-090 (tests/ по cancel_task/cancel.py), эта задача его НЕ меняет (docs-only).
notes: >
Docs-only задача: src/** не меняется, поэтому юнит/интеграционных тестов кода нет —
только структурные тесты документа. Полный регресс tests/ должен оставаться зелёным
(новый тест читает лишь файлы docs/, на src/-покрытие/coverage-baseline не влияет).
Все тесты — type: unit (без сети/LLM/subprocess), модуль tests/test_faq_stop_doc.py.
tests:
- id: TC-01
type: unit
description: "FAQ существует: docs/operations/FAQ_STOP.md присутствует, непустой, есть H1 про STOP и вводный абзац для пользователя (AC-1)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-02
type: unit
description: "Обязательные секции-якоря присутствуют: все 8 тем FR-2 (что делает STOP / как отменить / пошагово / отложенная отмена / не откатывает прод / перезапуск To Analyse / 'ничего не произошло' / где увидеть) (AC-2)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-03
type: unit
description: "Пошаговые последствия и сохранность: упомянуты остановка агента, снятие job'ов, удаление рабочей ветки/worktree, переход в cancelled, уведомление Telegram+Plane, явное 'docs сохраняются' (AC-3)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-04
type: unit
description: "Критичное окно: присутствует факт отложенной отмены (deferred / 'отложена') и явное 'main/прод не трогаются' (AC-4)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-05
type: unit
description: "STOP ≠ откат прод-кода: присутствует явный отрицательный ответ ('не откатывает' влитый в main/прод код) (AC-5)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-06
type: unit
description: "Перезапуск: упомянуто 'To Analyse' и создание задачи 'с нуля', отсутствует обещание 'продолжить с середины' (AC-6)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-07
type: unit
description: "Негативный скан фактов: в FAQ НЕТ запрещённых утверждений — 'откатит прод', 'трогает main/force-push', 'продолжить отменённую с середины', 'мгновенно убивает деплой' (AC-9)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-08
type: unit
description: "Перекрёстные ссылки: business.md (Сценарий 6) и tech-pipeline.md (Отмена: STOP → cancelled) содержат ссылку на FAQ_STOP.md; FAQ ссылается на инженерный обзор/ADR ORCH-090 (AC-8)."
module: tests/test_faq_stop_doc.py
expected: PASS
- id: TC-09
type: unit
description: "Docs-only регресс-инвариант: полный прогон tests/ зелёный; новый тест не импортирует src/ рантайм и не делает сети/subprocess (AC-10, AC-11)."
module: tests/test_faq_stop_doc.py
expected: PASS

View File

@@ -0,0 +1,173 @@
---
work_item: ORCH-108
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# ADR-001: Размещение пользовательского FAQ по STOP и контур анти-дрейфа
Work Item: **ORCH-108** — FAQ: как использовать статус STOP для отмены задачи
Стадия: **architecture**
Сквозная регистрация: **N/A — локальное решение задачи** (docs-only; новых QG/стадий/
компонентов/таблиц нет, маркеры `ORCH-NNN` в `src/**` не вводятся → сквозной
`docs/architecture/adr/adr-NNNN-*` не требуется; критерий — `docs/_standards/PIPELINE_DOCS.md` §4).
## Статус
Proposed
## Контекст
Механизм отмены задачи через Plane-статус **STOP** реализован в ORCH-090
(`docs/architecture/adr/adr-0026-stop-cancel-task.md`, `src/cancel.py`,
`src/stage_engine.py::cancel_task`, `src/webhooks/plane.py`). Пользовательской
инструкции «как этим пользоваться» нет — упоминания STOP разрознены и адресованы разным
читателям (витрина `docs/overview/business.md` «Сценарий 6», инженерный обзор
`docs/overview/tech-pipeline.md` «Отмена: STOP → `cancelled`», глубокий ADR ORCH-090). Это
порождает неверную ментальную модель («STOP откатит мой код из прода» — **неверно**) и нагрузку
на оператора (self-hosting: один инстанс на все проекты).
Аналитик (BRD/TRZ/AC, `ready-for-review`) полностью описал требуемый артефакт и приложил готовый
черновик содержания (TRZ Приложение A). Это **docs-only** задача: `src/**`, `STAGE_TRANSITIONS`,
`QG_CHECKS`, `check_*`, схема БД — не меняются; поведение STOP фиксировано ORCH-090 и FAQ его лишь
**документирует**. Архитектурных решений по существу два: (1) куда положить FAQ в дереве доков и
(2) как структурно защитить его от дрейфа «доки ↔ код». Остальное — исполнение на стадии
development.
Факты, сверенные на ветке задачи (read-only):
- Цели перекрёстных ссылок **существуют**: `docs/overview/business.md` §«Сценарий 6: остановить
задачу» (стр. 96), `docs/overview/tech-pipeline.md` §«Отмена: STOP → `cancelled`» (стр. 122),
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`. Ссылки FR-4 не «висячие».
- Семантика разделов доков (ORCH-011, `adr-0039`): `overview/` — витрина «что за система»,
`architecture/` — инженерный справочник, `deployment/` — «как развернуть у себя»,
`operations/` — «как эксплуатировать наш прод» (runbook'и: `ONBOARDING.md`,
`PHANTOM_MERGE_RUNBOOK.md`, `STAGING.md`, …).
- `docs/overview/`**курируемый плоский каталог из 10 файлов**, чьё содержимое прибито
структурным тестом `tests/test_system_docs.py` (витрина — не свалка произвольных доков).
- Прецедент анти-дрейф теста документа — `tests/test_lite_setup_doc.py` (детерминированный,
offline; позитивные якоря-секции + «кирпичи» + кросс-ссылки + негативный скан запрещённых
литералов по `FORBIDDEN`).
## Решение
### Сводка
Размещаем FAQ как **`docs/operations/FAQ_STOP.md`** — пользовательский документ «вопрос → ответ»,
прилинкованный из витрины/обзора и закрытый детерминированным структурным тестом
`tests/test_faq_stop_doc.py`. Утверждаем разумный дефолт аналитика (A1) как архитектурное решение,
с явной фиксацией ключевого нюанса теста — **негативный скан проверяет запрещённые
утверждения, а не голые подстроки** (иначе он ложно срабатывал бы на предложениях, которые эти же
термины корректно **отрицают**).
### D1 — Размещение: `docs/operations/FAQ_STOP.md` (BR-1, A1)
FAQ ложится в `docs/operations/` рядом с операторскими runbook'ами.
Обоснование выбора между тремя кандидатами (аудитория FAQ «пользователь доски + оператор»
неоднородна, поэтому секция не очевидна):
- **`docs/overview/` — отвергнуто.** Это курируемая витрина фиксированного состава (10 файлов),
защищённая `tests/test_system_docs.py`; добавление отдельного FAQ нарушит инвариант каталога
витрины и саму семантику «обзор, а не справочник процедур».
- **Новый раздел `docs/faq/` — отвергнуто.** Заведение top-level раздела ради одного документа —
scope-creep; нет канона/индекса/норматива сопровождения для нового раздела.
- **`docs/operations/FAQ_STOP.md` — выбрано.** Это де-факто дом человеко-ориентированных процедур
и «что делать, если…» (тробл-шутинг STOP в FR-2 п.7 ссылается на `stop_status_enabled`/
`stop_status_repos`, а «где увидеть результат» в п.8 — на read-only блок `stop` в `GET /queue`;
обе темы — операторская территория). Пользователь доски и оператор на self-hosting сильно
пересекаются; именно к operations-доке оператор отсылает пользователя.
Документированная остаточная издержка: лёгкое несоответствие «аудитория-пользователь ↔
секция-operations». Принимается осознанно (см. «Последствия»); пере-размещение в будущий
`docs/faq/` остаётся дешёвым (один файл + правка двух ссылок + одного теста).
### D2 — Граница объёма: docs-only, без рантайм-поверхности (NFR-1, AC-10)
Подтверждаю и фиксирую как архитектурный инвариант:
- `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict ключи, схема БД — **не
трогаются**; kill-switch не требуется (нет исполняемого кода).
- **`07-infra-requirements.md` — N/A** (топология/контейнеры/сеть не меняются).
- **`08-data-requirements.md` — N/A** (таблиц/колонок/индексов не добавляется).
- `docs/architecture/README.md` / `internals.md`**не обновляются**: задача не затрагивает
стадии/QG/компоненты (новый operations-FAQ описывает уже задокументированную фичу ORCH-090, не
вводя архитектурных сущностей). Внесение FAQ в архитектурный справочник было бы дублированием
источника истины (нарушение NFR-5).
- Coverage-гейт (ORCH-027): docs-only не добавляет строк `src/` → базовая линия покрытия не
меняется; `tests/test_faq_stop_doc.py` — структурный тест документа, `src/` не покрывает и на
метрику не влияет.
### D3 — Контур анти-дрейфа: `tests/test_faq_stop_doc.py`, негативный скан на уровне утверждений (NFR-4, FR-6, AC-11)
Структурный тест по образцу `tests/test_lite_setup_doc.py` — детерминированный, **без сети/LLM/
subprocess**, только парсинг файла. Обязательный состав проверок:
1. **Существование** `docs/operations/FAQ_STOP.md`.
2. **Позитивные якоря** — все 8 обязательных секций-вопросов TRZ §FR-2 присутствуют (заголовки —
стабильные якоря; тест матчит по нормализованному заголовку, не по точной пунктуации).
3. **«Кирпичи»-факты** — присутствуют ключевые токены (`STOP`, `cancelled`, «To Analyse»,
«отлож…»/`deferred`, упоминание `GET /queue`/блока `stop`).
4. **Кросс-ссылки** (FR-4) — ссылка на `tech-pipeline.md` и на ADR ORCH-090 присутствует.
5. **Негативный скан (КЛЮЧЕВОЙ нюанс).** Запрещённые **утверждения** FR-5 («STOP откатывает
прод», «STOP трогает `main`/force-push», «продолжить с середины», «STOP мгновенно убивает
деплой») детектируются как **утверждения целиком**, а **НЕ** как голые подстроки. Причина:
корректный FAQ закономерно содержит слова `main`, «откатыва…», «force-push», «деплой» внутри
**отрицающих** предложений («STOP **не откатывает**`main`»). Наивный substring-скан по этим
словам ложно завалит именно те фразы, которые требование AC-9 предписывает иметь. Реализация:
матчить нормативно-запрещённые фразы (например, утверждение отката прод-кода **без**
отрицания рядом), либо проверять, что запрещённый токен встречается только в соседстве с
отрицанием. Конкретную форму выбирает разработчик; инвариант — **тест не должен фолзить на
фактически верном FAQ** и **обязан краснеть на реально ложном утверждении**.
Контракт теста — никогда не делать сеть/LLM/subprocess (как и эталон), чтобы оставаться частью
обычного зелёного `tests/` без инфра-зависимостей.
### D4 — Целостность ссылок и link-first (FR-4, NFR-5, AC-8)
Перекрёстные ссылки добавляются **в обе стороны** (витрина/обзор → FAQ; FAQ → обзор + ADR
ORCH-090). Источник истины поведения остаётся за ADR ORCH-090 и инженерным обзором — FAQ их
**не форкает** (машинные детали: маркеры/lease/тумбстон — только ссылками). Цели ссылок
проверены существующими (см. Контекст). Якорь-слаг на секцию обзора
(`tech-pipeline.md` «Отмена: STOP → `cancelled`») разработчик обязан сверить с фактической
генерацией якоря при переносе (риск TR-4).
### D5 — Норматив сопровождения (traceability)
Фиксируется правило: **правишь поведение STOP** (`src/cancel.py` / `cancel_task` / маршрут `stop`
в `src/webhooks/plane.py`) → **обнови `docs/operations/FAQ_STOP.md` в том же PR** (правило агентов
№2/№6; reviewer-ось «документация»). Машинный маркер `ORCH-108` в `src/**` НЕ вводится (docs-only),
поэтому анти-археологии маркеров (`docs/_standards/TRACEABILITY.md`) этот PR не порождает; связь
«код STOP ↔ FAQ» держится нормативом сопровождения + структурным тестом D3.
## Альтернативы
- **FAQ в `docs/overview/`** — отвергнуто: курируемая витрина фиксированного состава под
`tests/test_system_docs.py`; FAQ ≠ обзорный слайд (см. D1).
- **Новый раздел `docs/faq/`** — отвергнуто: scope-creep ради одного файла (см. D1).
- **Без анти-дрейф теста, полагаясь на reviewer** — отвергнуто: NFR-4 требует структурной
защиты от дрейфа «доки ↔ код»; ручная проверка не воспроизводима.
- **Негативный скан по голым подстрокам** — отвергнуто: ложные срабатывания на корректно
отрицающих предложениях (см. D3) — это сделало бы тест либо красным на верном FAQ, либо
вынудило бы выкинуть из FAQ обязательные явные отрицания.
- **Сквозной (global) ADR** — отвергнуто: решение не кросс-каттинговое (нет нового QG/стадии/
компонента/таблицы; не меняет канон доков как такой).
## Последствия
- **+** Единый самодостаточный источник для пользователя доски → меньше неверных ожиданий и
обращений к оператору (self-hosting-выгода).
- **+** Структурный тест (D3) делает дрейф «доки ↔ код» воспроизводимо ловимым; норматив D5
закрывает процессный пробел.
- **+** Нулевой рантайм-риск: docs-only, прод-деплой этой задачи безопасен.
- **** Лёгкое несоответствие «пользовательская аудитория ↔ секция operations» (D1). Митигейшн:
явный вводный абзац «для кого» в FAQ + дешёвое будущее пере-размещение.
- **** Риск чрезмерно строгого негативного скана (D3). Митигейшн: матч на уровне утверждений +
явный инвариант «не фолзить на верном FAQ» (TR-3).
- **Откат:** удалить `docs/operations/FAQ_STOP.md` и `tests/test_faq_stop_doc.py`, снять
добавленные ссылки из `business.md`/`tech-pipeline.md` и запись из `CHANGELOG.md`. Рантайм не
затрагивается.
## Ссылки
- BRD: `docs/work-items/ORCH-108/01-brd.md`
- TRZ: `docs/work-items/ORCH-108/02-trz.md` (+ Приложение A — черновик содержания FAQ)
- Acceptance: `docs/work-items/ORCH-108/03-acceptance-criteria.md`
- Tech-risks: `docs/work-items/ORCH-108/10-tech-risks.md`
- Источник истины поведения STOP: `docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`,
`docs/architecture/adr/adr-0026-stop-cancel-task.md`
- Сверено по коду: `src/cancel.py`, `src/stage_engine.py::cancel_task`,
`src/webhooks/plane.py` (`handle_issue_updated`/`handle_stop`/`handle_status_start`),
`src/config.py` (`stop_status_enabled`/`stop_status_repos`), `src/main.py` (блок `stop` в
`GET /queue`)
- Эталон анти-дрейф теста: `tests/test_lite_setup_doc.py`
- Семантика разделов доков: `docs/architecture/adr/adr-0039-system-overview-docs-canon.md`

View File

@@ -0,0 +1,39 @@
---
work_item: ORCH-108
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-108 — FAQ по статусу STOP
Work Item: **ORCH-108** · Repo: **orchestrator** (self-hosting) · Стадия: architecture
> Информационный документ (гейтом не парсится). Перечисляет риски реализации **docs-only**
> задачи и их митигейшн. Класс рисков — минимальный: рантайм/конвейер не затрагиваются.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Дрейф «доки ↔ код».** Будущая правка поведения STOP (`src/cancel.py`/`cancel_task`/маршрут `stop`) сделает FAQ неверным. | Сред. | Сред. | Структурный анти-дрейф тест `tests/test_faq_stop_doc.py` (ADR D3) + норматив сопровождения «правишь STOP → обнови FAQ в том же PR» (ADR D5) + reviewer-ось «документация». |
| TR-2 | **FAQ-«сирота» / дубль источника истины.** FAQ не связан с витриной или дословно копирует ADR/обзор вместо ссылки. | Низ. | Низ. | Link-first (ADR D4): двусторонние ссылки (AC-8), машинные детали — только ссылками; тест проверяет наличие кросс-ссылок. |
| TR-3 | **Ложно-строгий негативный скан.** Тест ищет запрещённые слова (`main`, «откатыва…», `force-push`) как голые подстроки → краснеет на корректно **отрицающих** предложениях FAQ (которые AC-9 предписывает иметь). | Сред. | Сред. | Негативный скан — на уровне **утверждений**, а не подстрок (ADR D3); инвариант «тест не фолзит на верном FAQ, но краснеет на реально ложном». Зеркало эталона `tests/test_lite_setup_doc.py`. |
| TR-4 | **Битый якорь кросс-ссылки.** Ссылка `tech-pipeline.md#отмена-stop--cancelled` не совпадёт с фактически генерируемым slug заголовка «Отмена: STOP → `cancelled`». | Низ. | Низ. | Разработчик сверяет slug при переносе (ADR D4); цели секций подтверждены существующими (business.md §«Сценарий 6», tech-pipeline.md §«Отмена», ADR ORCH-090). |
| TR-5 | **Фактическая неточность FAQ.** Утверждение расходится с кодом (напр. «STOP откатит прод», «убивает деплой мгновенно»). | Низ. | Выс. | NFR-2/FR-5/AC-9: каждое утверждение verifiable против read-only модулей (ADR §Ссылки); reviewer сверяет с кодом; негативный скан (TR-3) ловит запрещённый класс. Содержание выверено аналитиком (TRZ Приложение A). |
| TR-6 | **Ошибочное размещение раздела.** Аудитория FAQ — «пользователь доски», секция — `operations/` («наш прод»). | Низ. | Низ. | Осознанный компромисс (ADR D1): альтернативы (`overview/` под тестом витрины, новый `docs/faq/`) хуже; вводный абзац «для кого»; будущее пере-размещение дёшево (1 файл + 2 ссылки + 1 тест). |
## Сводный вывод
Доминирующий класс — **дрейф документации** (TR-1) и **хрупкость анти-дрейф теста** (TR-3); оба
структурно снижены решением D3 (claim-level негативный скан + детерминированный offline-тест) и
нормативом сопровождения D5. Рантайм-рисков **нет**: задача docs-only, не трогает `src/**`/
`STAGE_TRANSITIONS`/`QG_CHECKS`/схему БД, не деплоит/не рестартит прод/не трогает `main`
self-hosting-безопасна, прод-деплой безвреден.
**Эскалация не требуется.** Не `arch:major-change` (нет новой стадии/компонента/смены БД), возврат
в анализ не нужен (BRD/TRZ/AC полны и согласованы с кодом; блокирующих неоднозначностей нет —
`01-questions.md` аналитиком осознанно не создан). Остаточный риск для прод-конвейера —
**пренебрежимо мал**.

View File

@@ -0,0 +1,85 @@
---
verdict: APPROVED
work_item: ORCH-108
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-17
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-108
version: 1
---
# Review ORCH-108 — FAQ: как использовать STOP для отмены задачи
## Summary
Docs-only задача: создаёт пользовательский FAQ `docs/operations/FAQ_STOP.md` (формат «вопрос →
ответ»), двусторонние перекрёстные ссылки витрина/обзор ⇄ FAQ ⇄ ADR ORCH-090 и детерминированный
анти-дрейф тест `tests/test_faq_stop_doc.py`. Поведение STOP — источник истины ORCH-090
(`adr-0026`) — **не меняется**.
Проверены все 4 оси. **Соответствие ТЗ/AC (111)** — полное. **Соответствие ADR** — все решения
D1D5 реализованы. **Качество** — тест содержателен, детерминирован, с non-evergreen-самочеком.
**Документация (приоритетная ось)** — CHANGELOG обновлён, витрина `docs/overview/` обновлена, ADR
заведён, `src/**` не тронут → нет необновлённой документации. **P0/P1 findings отсутствуют.**
Верификация по ключевым осям:
- **AC-9 (фактическая корректность) — самая важная для docs-FAQ.** Все 9 утверждений FAQ сверены с
кодом (`src/stage_engine.py::cancel_task`, `src/cancel.py`, `src/webhooks/plane.py`,
`src/gitea.py`, `src/db.py`) — **каждое CONFIRMED**, противоречий коду нет: graceful SIGTERM-стоп
(`launcher.stop_process`); job'ы → терминальный `cancelled`, не реквью'ятся (`claim_next_job`
берёт только `queued`); удаление worktree + рабочей ветки с guard `_PROTECTED_BRANCHES={main,
master}` (никогда `main`/force-push); docs-артефакты сохраняются; отложенная отмена в критичном
окне (`cancel.in_critical_window` → только `queued`-job'ы, running-актор не трогается, finalizer
применяет позже); STOP не делает revert влитого; релонч гейтится строго стадией `analysis`
(дыра релонча ORCH-090 D6 закрыта); fail-closed no-op (нет `stop` в `_DEFAULT_STATES`) +
идемпотентный no-op для терминальной задачи + kill-switch `stop_status_enabled`/`stop_status_repos`.
- **AC-8 / TR-4 (риск висячего якоря).** Внутренняя ссылка FAQ `…tech-pipeline.md#отмена-stop--cancelled`
корректно резолвится в заголовок `## Отмена: STOP → \`cancelled\`` (slug с двойным дефисом от
удалённого `` — совпадает байт-в-байт). Цель ADR-ссылки
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md` существует. Обратные ссылки из
`business.md` (Сценарий 6) и `tech-pipeline.md` (Отмена: STOP → `cancelled`) присутствуют.
- **AC-10 / AC-11.** `git diff origin/main...HEAD`: только `docs/**`, `CHANGELOG.md`,
`tests/test_faq_stop_doc.py` (+ scratch `.task-dev.md`); `src/**` / `STAGE_TRANSITIONS` /
`QG_CHECKS` / `check_*` / схема БД — не тронуты. `tests/test_faq_stop_doc.py` — 12 passed;
`tests/test_system_docs.py` (витрина) — 29 passed.
## Findings
### P0 — Blocker
- _нет_
### P1 — Must fix
- _нет_
### P2 — Should fix
- _нет_
### P3 — Nice-to-have
- [ ] `.task-dev.md` (scratch-файл dev-трекинга в корне) попал в коммит, обновлён с `ORCH-126` на
`ORCH-108`. Это существующий трекируемый файл, к deliverables не относится, рантайм/конвейер не
затрагивает — инконсеквентно, фиксации не требует. Отмечено только для полноты.
## Документация
Приоритетная ось пройдена. Это **docs-задача**, `src/**` не изменён → требование «изменил `src/` →
обнови доку в том же PR» не активируется; при этом всё, что задача обязана обновить, обновлено:
- **`CHANGELOG.md`** — запись ORCH-108 присутствует (раздел `[Unreleased]`), с инвариантом docs-only
и нормативом сопровождения. ✓
- **Витрина системы `docs/overview/` (ORCH-011)** — `business.md` (Сценарий 6) и `tech-pipeline.md`
(Отмена: STOP → `cancelled`) дополнены ссылкой на FAQ; `tests/test_system_docs.py` зелёный
(инвариант курируемого каталога витрины не нарушен — FAQ положен в `docs/operations/`, не в
`docs/overview/`, см. ADR D1). ✓
- **ADR** — `docs/work-items/ORCH-108/06-adr/ADR-001-faq-stop-placement-and-anti-drift.md` заведён;
сквозной global-ADR обоснованно N/A (локальное docs-only решение, нет нового QG/стадии/компонента/
таблицы — критерий `PIPELINE_DOCS.md` §4). ✓
- **README «Известные ограничения» (ORCH-079)** — ORCH-108 не закрывает ни один из открытых пунктов
(Telegram 48h / intra-repo deps / пакетный автоном); STOP уже документирован в README §«Отмена
задачи: статус STOP». Обновление README не требуется. ✓
- **link-first (ADR D4)** — машинные детали (`тумбстон`/`merge-lease`/`_ensure_column`) в FAQ не
дублируются, даются ссылками; проверено тестом (`test_faq_links_back_to_overview_and_adr`). ✓
Документация = golden source: обновлена корректно и согласованно. Нет необновлённой документации →
блокирующего finding'а по этой оси нет.

View File

@@ -0,0 +1,40 @@
---
result: PASS
work_item: ORCH-108
stage: testing
author_agent: test-runner
status: success
created_at: 2026-06-17
model_used: n/a
exit_code: 0
smoke: ok
---
# Test Gate Log (deterministic runner, ORCH-116)
pytest exit-code `0` -> `result: PASS` (smoke: ok).
Вердикт зафиксирован детерминированным test-раннером (ORCH-116), не LLM. PASS/FAIL = exit-код `pytest` + read-only smoke (`/health`, `/status`, `/queue` + блок `serial_gate`).
pytest stdout (tail):
```
.................................................................... [ 64%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 74%]
........................................................................ [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
................................................................... [100%]
=============================== warnings summary ===============================
src/config.py:8
/repos/_wt/orchestrator/feature_ORCH-108-19c40858/src/config.py:8: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
class Settings(BaseSettings):
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
2227 passed, 1 warning in 99.72s (0:01:39)
```

View File

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

View File

@@ -0,0 +1,46 @@
---
staging_status: SUCCESS
work_item: ORCH-108
stage: deploy-staging
author_agent: staging-runner
status: success
created_at: 2026-06-17
model_used: n/a
exit_code: 0
base_url: http://localhost:8501
---
# Staging Gate Log (deterministic runner, ORCH-115)
Staging suite exit-code `0` -> `staging_status: SUCCESS`.
Вердикт зафиксирован детерминированным staging-раннером (ORCH-115), не LLM. infra-tolerance (ORCH-061) уже учтена внутри `staging_check.py` — раннер её не пересуживает.
INFRA-WAIVED lines (ORCH-061, copied for observability):
- INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
Staging suite stdout (tail):
```
(waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
· waiting... (waiting for analyst job in queue)
✗ FAIL C9b Analyst job enqueued in staging queue
[CLEANUP]
· CLEANUP: no branch to delete
✓ PASS CLEANUP: deleted Plane issue a38f627e-4ba4-47c3-a19f-3bb939a79a37 (HTTP 204)
· CLEANUP DB: no task row found for plane_id=a38f627e-4ba4-47c3-a19f-3bb939a79a37
· CLEANUP DB dedup: no such table: events_dedup
============================================================
 RESULT: 8/10 checks PASS
REAL failed : none
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
============================================================
· tolerance: staging_infra_tolerance_enabled=True
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```

View File

@@ -0,0 +1,7 @@
# Business Request: BUG: failed/cancelled task artifacts must be cleaned from shared checkout
Work Item ID: ORCH-112
## Description
TBD

View File

@@ -0,0 +1,169 @@
---
work_item: ORCH-112
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 01 — BRD / Bug-report: ORCH-112 — failed/cancelled task artifacts must be cleaned from shared checkout
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в full-cycle**
> ⚠️ **`escalate: full-cycle` (ADR-001 D5 ORCH-019).** Баг помечен `Bug`, но по сути это
> **архитектурный + safety-critical (self-hosting)** дефект: правка лежит в самом опасном пути
> прод-деплоя (хост-хук, прямо перед рестартом прод-контейнера) и требует **решения о политике
> жизненного цикла** shared checkout (ADR). Поэтому выпускается **полный** analysis-пакет, а не
> облегчённый bug-пакет. Оператор снимает багфикс-трек: `POST /bug-fast-track/escalate?work_item=ORCH-112`
> → задача пойдёт через стадию `architecture` (architect выпустит ADR для политики cleanup/изоляции).
---
## 1. Бизнес-контекст и проблема
### Симптом (наблюдаемое)
Self-deploy задачи **ORCH-111** упал на шаге `git pull origin main` хост-хука деплоя с ошибкой:
```
error: Your local changes to the following files would be overwritten by merge:
src/config.py
Please commit your changes or stash them before you merge.
```
Деплой прерван, конвейер потребовал **ручного вмешательства оператора** (на self-hosting это
групповой риск — встаёт деплой и всех других проектов).
### Причина симптома (установленный факт)
В **общем (shared) checkout** `/home/slin/repos/orchestrator` оставались грязные файлы от
ранее **неуспешной/отменённой/перезапущенной задачи ORCH-104** (тема Lite installer):
- модифицированный tracked-файл: `src/config.py`;
- модифицированный/untracked: `docs/deployment/LITE_SETUP.md`;
- untracked: `scripts/install_lite.py`, `tests/test_install_lite.py`,
`docs/deployment/lite-install.example.yaml`.
Через несколько дней эти остатки **заблокировали** `git pull` другой задачи (ORCH-111).
### Локализация (анализ — куда смотреть архитектору/разработчику)
**Установленный факт о топологии (CLAUDE.md / `docs/architecture/README.md`):**
`/home/slin/repos/orchestrator` (хост) == `/repos/orchestrator` (контейнер, bind-mount) ==
**main clone** (`settings.repos_dir/<repo>` = `settings.deploy_host_repo_path`). Это **deploy-база
и база управления worktree'ами**, а НЕ рабочая копия агента.
1. **Первичный дефект — нерезистентный `git pull`.**
`scripts/orchestrator-deploy-hook.sh:224-226` делает `cd "$REPO"` (= deploy-база) и
**голый** `git pull origin main` **без гигиены рабочего дерева**. Любая локальная правка
tracked-файла блокирует merge → деплой падает. Проверено: во всём `src/`+`scripts/` **нет ни
одного** `git reset --hard` / `git clean` / `git stash` для приведения базы к чистому состоянию.
Shared checkout трактуется как «всегда чистый», что не гарантировано.
2. **Невыполненный/неэнфорснутый инвариант + отсутствие «дворника».**
Нормальный конвейер **не пишет** в рабочее дерево main clone: агенты работают в изолированных
worktree'ах `/repos/_wt/<repo>/<branch>` (`git_worktree.ensure_worktree`); `docker build`
использует контекст **worktree** (`image_freshness._host_worktree_path`), не main clone;
fallback'и гейтов на main clone — **только чтение** (`git show origin/main:...`,
`qg/checks.py:451`, `coverage_gate.py:297`, `stage_engine.py:145`). Поэтому грязь ORCH-104
почти наверняка — **ручной/брошенный WIP** в shared checkout во время инцидента ORCH-104
(косвенное подтверждение: файлы `install_lite.py`/`test_install_lite.py`/`lite-install.example.yaml`
**никогда не существовали в git-истории** — закоммиченный артефакт ORCH-104 это
`scripts/setup_lite.py`, commit `e2cf883`). Вне зависимости от источника: **нет механизма**,
который детектирует/чистит грязную базу и **нет задокументированного/энфорснутого инварианта**
«main checkout — неизменяемая deploy-база, не workspace».
3. **`cancel_task` чистит worktree + remote-ветку, но НЕ shared checkout.**
`stage_engine.cancel_task` (шаг 3d, строки ~2330-2343): `remove_worktree(repo, branch)` +
`gitea.delete_remote_branch(repo, branch)`. Это корректно (конвейер в main clone не пишет), но
означает **нулевое покрытие** случая «грязная deploy-база» в каскадах failed/cancelled.
**Вывод:** даже если первопричина грязи — ручное действие, устойчивость должна быть на стороне
системы: deploy-база обязана **самовосстанавливаться** в чистый `origin/main` перед pull, а
политика жизненного цикла — гарантировать, что остатки failed/cancelled задач не клинят будущие
операции.
## 2. Объём (scope)
### В объёме
- Сделать self-deploy `git pull origin main` (shared deploy-база) **устойчивым к грязному рабочему
дереву** — приведение базы к чистому `origin/main` **автономно**, без ручного вмешательства.
- Гарантировать, что после **failed / cancelled / брошенной** задачи в shared checkout не остаётся
рабочих остатков, способных заблокировать будущий деплой/операцию (сходимость базы к чистому
`origin/main`).
- Задокументировать (и где осуществимо — мягко энфорснуть/гардить) инвариант
«shared main checkout — deploy/worktree-management база, НЕ редактируемый workspace».
- Наблюдаемость: лог + Telegram-алерт, когда deploy-база найдена грязной и автоочищена (или отказ).
### Вне объёма
- ❌ Запрет/контроль ручных операций оператора в shared checkout (вне технической власти системы;
закрываем устойчивостью, а не запретом).
- ❌ Изменение модели worktree per-task (`git_worktree`, ORCH-2) — она корректна и не трогается.
- ❌ Любое изменение `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключей / схемы БД.
- ❌ Изменение поведения деплоя на чистой базе (happy-path должен остаться байт-в-байт).
- ❌ Выбор конкретного механизма (reset --hard vs janitor vs guard) — это **зона архитектора** (ADR).
## 3. Заинтересованные стороны
- **Заказчик/оператор (Слава)** — страдает от ручного разруливания залипших деплоев; принимает результат.
- **Self-hosting конвейер orchestrator** — прямой потребитель (надёжность прод-деплоя).
- **Все проекты на общем инстансе (enduro-trails)** — косвенно: залипший self-deploy орка
останавливает обслуживание их задач.
## 4. Бизнес-требования (BR)
- **BR-1** — Грязное рабочее дерево shared deploy-базы (модифицированные tracked-файлы и/или
untracked-файлы) **НЕ должно блокировать** self-deploy `git pull origin main`: деплой обязан
привести базу к чистому, актуальному `origin/main` **без ручного вмешательства**.
- **BR-2** — После failed / cancelled / брошенной задачи в shared checkout **не должно оставаться**
рабочих остатков этой задачи, способных заблокировать будущий деплой/git-операцию; база
**сходится** к чистому `origin/main`.
- **BR-3** — Инвариант «shared main checkout (`<host_repos_dir>/<repo>`) — deploy/worktree-management
база, НЕ workspace» должен быть **задокументирован** (`docs/operations/INFRA.md` +
`docs/architecture/README.md`) и, где осуществимо, **энфорснут/гардирован**; конвейер/агенты
**никогда** не пишут рабочие изменения в main clone (верифицировать, что это так).
- **BR-4** — **Наблюдаемость:** обнаружение грязной базы и факт автоочистки (или отказ) должны
логироваться и алертиться (Telegram, кликабельный номер) — оператор видит, что гигиена сработала.
- **BR-5** — На **чистой** базе поведение деплоя — **байт-в-байт прежнее** (обычный fast-forward
`git pull`); никакого регресса happy-path.
## 5. Нефункциональные требования (NFR)
- **NFR-1 (self-hosting safety)** — гигиена **никогда** не трогает ветку `main` на remote, не делает
force-push, не рестартит прод-контейнер вне штатного гейта, не удаляет worktree/ветки **других
активных** задач. Оперирует **только** настроенным путём deploy-базы.
- **NFR-2 (сохранность deploy-состояния)** — автоочистка **не должна** удалять артефакты, легитимно
живущие под `$REPO`/рядом: rollback-снимки `$REPO/.deploy-prev-image-*`
(`deploy_prod_prev_image_file`), `deploy-hook.log`, sibling-состояния
`<repos_dir>/.deploy-state-*` / `.merge-lease-*.json`, и админ-записи worktree в `.git/worktrees`.
(Наивный `git clean -xfd` в `$REPO` уничтожил бы `.deploy-prev-image-*` и сломал rollback — это
**жёсткое ограничение** для архитектора/разработчика.)
- **NFR-3 (обратимость / kill-switch)** — новое поведение под флагом; выключенный флаг → деплой
байт-в-байт как до ORCH-112 (голый `git pull origin main`).
- **NFR-4 (надёжность)** — never-raise / fail-safe (по образцу leaf'ов `serial_gate`/`cancel`);
идемпотентность; restart-safe; сбой гигиены не должен маскировать или ухудшать исход деплоя сверх
текущего.
- **NFR-5 (нулевая регрессия конвейера)** — `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` /
machine-verdict ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт**.
- **NFR-6 (область)** — изменение скоупится на self-hosting (`orchestrator`); поведение для прочих
репо/синхронного деплоя агентом — не ухудшается.
## 6. Допущения и ограничения
- Shared checkout и хост-хук физически разделяют один путь с контейнером через bind-mount
(`repos_dir``host_repos_dir`); хук исполняется на **хосте** по ssh (ORCH-036, detached).
- Build-once путь (`SOURCE_IMAGE` retag) **не** зависит от содержимого рабочего дерева main clone —
прод получает ровно staging-валидированный образ; значит дискард рабочего дерева base перед pull
**безопасен для деплоимого артефакта**. (`--build-staging` собирается из **worktree**, не из main —
отдельный контур.)
- Источник истины кода — `origin/main`; локальные правки в deploy-базе **по определению** не должны
существовать (это deploy-база, а не место работы).
- Конкретный механизм (resilient pull через reset+clean со скоуп-исключениями / активный janitor /
guard инварианта / комбинация) — **открытый вопрос для архитектуры**, решается в `06-adr/`.
## 7. Критерии успеха
Self-deploy успешно выполняет `git pull` на ранее грязной shared-базе **без ручного вмешательства**;
deploy-база сходится к чистому `origin/main`; rollback-состояние и sibling-артефакты сохранены;
happy-path и весь конвейер — без регресса; обязательный регресс-тест **красный до фикса, зелёный
после**. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
## 8. Риски
- Деструктивная гигиена (`reset --hard`/`clean`) в **прод-deploy-базе** рядом с рестартом прода —
ошибка скоупа может удалить rollback-state/логи (см. NFR-2) → ADR обязателен.
- Маскировка реальной первопричины: если в будущем какой-то код **начнёт** писать в main clone,
«тихая автоочистка» это скроет → нужна наблюдаемость (BR-4).
- Кросс-каттинг с ORCH-036 (self-deploy), ORCH-058 (image-freshness/provenance), ORCH-090 (cancel),
ORCH-2 (worktree-модель). Детали/митигации — `10-tech-risks.md` (заполняет архитектор).

View File

@@ -0,0 +1,110 @@
---
work_item: ORCH-112
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-112 — failed/cancelled task artifacts must be cleaned from shared checkout
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **требования и ограничения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное **решение** (какой механизм гигиены/изоляции выбрать) — задача архитектора (`06-adr/`),
> т.к. задача эскалирована в full-cycle (`01-brd.md` → `escalate: full-cycle`).
## 1. Сводка изменения
Сделать self-deploy устойчивым к **грязной shared deploy-базе** и гарантировать сходимость базы к
чистому `origin/main` после failed/cancelled/брошенных задач. Корень симптома — голый
`git pull origin main` в `scripts/orchestrator-deploy-hook.sh` (строка 226), исполняемый в
`$REPO` (= `settings.deploy_host_repo_path` = main clone), который падает при любой локальной правке
tracked-файла. Требуется: (а) приведение deploy-базы к чистому `origin/main` перед/в момент pull
**без ручного вмешательства**, со строгим сохранением deploy-rollback-состояния; (б) документирование
+ (по возможности) энфорс инварианта «main checkout — deploy-база, не workspace»; (в) наблюдаемость.
## 2. Задействованные модули / пути
| Путь | Действие | Примечание |
|------|----------|-----------|
| `scripts/orchestrator-deploy-hook.sh` | изменить | строки 224-226: голый `git pull origin main` в `$REPO` — точка отказа (FR-1) |
| `src/self_deploy.py` | возможно изменить | `build_deploy_command` / `initiate_deploy` / `rebuild_staging_image` строят инвокацию хука — возможная точка передачи гигиены/флага (решает архитектор) |
| `src/stage_engine.py` | возможно изменить | `cancel_task` (шаг 3d, ~2330-2343) — каскад cancel; расширение гигиены на shared-базу (FR-2, если выбран этот путь) |
| `src/git_worktree.py` | возможно изменить | модель main clone ↔ worktree; возможный helper приведения базы к чистоте / верификация инварианта (BR-3) |
| `src/config.py` | изменить | новый kill-switch + флаги области (FR-5) |
| `src/<new_leaf>.py` (напр. `checkout_hygiene.py`) | возможно создать | чистый never-raise leaf политики гигиены (по образцу `serial_gate`/`cancel`) — **создавать ли** решает архитектор |
| `docs/operations/INFRA.md` | изменить | инвариант «shared checkout — deploy-база, не workspace» (BR-3) |
| `docs/architecture/README.md` | изменить | описать политику гигиены/жизненного цикла deploy-базы |
| `CHANGELOG.md`, `CLAUDE.md` | изменить | правило «docs = golden source» (CLAUDE.md §2) |
| `tests/test_<...>.py` | создать | регресс + покрытие (см. `04-test-plan.yaml`) |
## 3. Функциональные требования
### FR-1 — Устойчивый self-deploy `git pull` (BR-1, BR-5)
- На пути self-deploy (`scripts/orchestrator-deploy-hook.sh`, шаг «2. Pull latest code»)
`git pull origin main` **не должен падать** из-за грязного рабочего дерева `$REPO`.
- Перед обновлением база приводится к чистому, актуальному `origin/main` (приведение tracked- и
untracked-изменений к состоянию `origin/main`), **с сохранением** артефактов из NFR-2.
- На **уже чистой** базе результат — обычный fast-forward; наблюдаемое поведение и exit-коды
(0/1/2, ORCH-036) — **байт-в-байт прежние** (BR-5).
- Контракт: never-break — сбой шага гигиены не должен ухудшать исход относительно текущего голого
pull (fail-safe).
### FR-2 — Сходимость shared-базы после failed/cancelled/брошенной задачи (BR-2)
- После терминации задачи (`failed` job-исход / `cancelled` через STOP / брошенный WIP) в shared
checkout **не остаётся** рабочих остатков, способных заблокировать будущий деплой/git-операцию.
- Допустимая трактовка «сходимости» (на выбор архитектора, **не** прескриптивно здесь): автоочистка
непосредственно в self-deploy перед pull (FR-1) **и/или** активный «дворник», приводящий
`<host_repos_dir>/<repo>` к чистому `origin/main`.
- Каскад `cancel_task` (ORCH-090) уже чистит **worktree + remote-ветку**; расширение на shared-базу
(если выбрано) делается тем же never-raise best-effort способом.
### FR-3 — Инвариант deploy-базы (BR-3)
- Задокументировать: `<host_repos_dir>/<repo>` — deploy/worktree-management база; рабочие изменения
туда **не пишутся** конвейером/агентами (агенты — worktree `git_worktree`; build — worktree-контекст;
fallback'и гейтов — read-only `git show origin/main`).
- Верифицировать, что текущий код этот инвариант соблюдает (анализ ORCH-112: соблюдает; единственные
обращения к main clone — read-only/fetch/worktree-управление). Где осуществимо — добавить
лёгкий guard/проверку (решает архитектор), **без** изменения горячих путей.
### FR-4 — Наблюдаемость (BR-4)
- Обнаружение грязной deploy-базы и факт автоочистки (число/имена сброшенных путей) или **отказ**
гигиены — лог (структурная запись) + Telegram-алерт (`send_telegram`, кликабельный номер задачи,
best-effort, never-raise). Опционально — read-only снапшот в `GET /queue` (решает архитектор).
### FR-5 — Условность / kill-switch (BR-5, NFR-3, NFR-6)
- Новое поведение под **kill-switch** (env `ORCH_*`, по образцу `serial_gate_enabled`/`stop_status_enabled`);
выключенный флаг → деплой байт-в-байт прежний (голый `git pull origin main`).
- Область — self-hosting (`orchestrator`); прочие репо/синхронный деплой агентом — не ухудшаются.
- `applies(repo)` (локальный, без сети) проверяется первым.
## 4. Изменения API
**Нет** обязательных. Опционально (на усмотрение архитектора) — read-only блок (напр. `checkout_hygiene`)
в существующем `GET /queue` для наблюдаемости. Новых управляющих эндпоинтов не требуется.
## 5. Изменения схемы БД
**Нет.** Состояние гигиены, если нужно, — in-memory / sentinel-файлы (паттерн `self_deploy`/`merge_gate`),
без миграции БД. Аддитивная таблица не требуется.
## 6. Требования к новым/изменённым QG checks
**Нет.** Это **не** Quality Gate и не стадия — это устойчивость deploy-пути и политика гигиены.
`STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict ключи
(`deploy_status:`/`staging_status:`/…) — **байт-в-байт не тронуты** (NFR-5).
## 7. Совместимость / регресс
- **Обратная совместимость:** kill-switch off → голый `git pull origin main` (1:1 до ORCH-112);
чистая база → fast-forward без изменений (BR-5).
- **Область раската:** self-hosting `orchestrator`; enduro/прочие — нулевая регрессия.
- **Обратимость:** выключение флага мгновенно возвращает прежнее поведение.
- **Сохранность (жёсткое ограничение, NFR-2):** гигиена **не удаляет** `$REPO/.deploy-prev-image-*`
(rollback), `deploy-hook.log`, sibling `<repos_dir>/.deploy-state-*` / `.merge-lease-*.json`,
админ-записи `.git/worktrees`. Любой `clean`-скоуп обязан их исключать.
- **Self-hosting инварианты (NFR-1):** никогда не трогать `main` на remote, не force-push, не
рестартить прод вне гейта, не сносить worktree/ветки других активных задач, оперировать только
настроенным путём deploy-базы. Exit-code-контракт хука (0/1/2) сохранён.
- **Артефакты pipeline:** создаются/обновляются обычные docs work item (`01..04` этой задачи,
`06-adr/` на стадии architecture после эскалации, `14-deploy-log.md` при деплое). Новых
pipeline-артефактов задача не вводит.
- **Трассировка (CLAUDE.md §9 / ORCH-078):** правки маркированных блоков (ORCH-036 self-deploy,
ORCH-058 image-freshness, ORCH-090 cancel) — сверять с их `06-adr/` перед изменением, инварианты
не ломать.

View File

@@ -0,0 +1,128 @@
---
work_item: ORCH-112
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-112 — failed/cancelled task artifacts must be cleaned from shared checkout
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Reviewer/CI проверяют их буквально по файлам репозитория и тестам.
---
## AC-1 — Грязная tracked-правка не блокирует self-deploy pull (регресс ORCH-104/ORCH-111)
**Условие:** shared deploy-база имеет локальную модификацию tracked-файла (напр. `src/config.py`),
self-deploy выполняет шаг pull.
- **PASS:** база приводится к чистому актуальному `origin/main` без ручного вмешательства; шаг pull
не возвращает «local changes would be overwritten by merge»; деплой продолжается; есть тест,
воспроизводящий точный сценарий ORCH-111 (**красный до фикса, зелёный после**).
- **FAIL:** pull/деплой падает на грязной tracked-правке; или сценарий не покрыт тестом.
---
## AC-2 — Untracked WIP-файлы не блокируют и не «протекают» в деплой
**Условие:** в shared-базе лежат untracked-файлы failed/брошенной задачи (напр.
`scripts/install_lite.py`, `tests/test_install_lite.py`, `docs/deployment/lite-install.example.yaml`).
- **PASS:** база сходится к чистому `origin/main`; untracked-остатки не блокируют операцию и не
попадают в деплоимый/собираемый артефакт.
- **FAIL:** untracked-остатки блокируют операцию либо остаются и клинят будущий деплой.
---
## AC-3 — Сохранность deploy-rollback-состояния и sibling-артефактов (жёсткое ограничение)
**Условие:** в `$REPO`/рядом присутствуют `.deploy-prev-image-*` (rollback), `deploy-hook.log`,
`<repos_dir>/.deploy-state-*`, `.merge-lease-*.json`, `.git/worktrees/*`.
- **PASS:** после гигиены **все** перечисленные артефакты на месте; rollback по
`.deploy-prev-image-*` остаётся работоспособным; есть тест, доказывающий их неудаление.
- **FAIL:** гигиена удаляет хотя бы один из этих артефактов (особенно `.deploy-prev-image-*`).
---
## AC-4 — Happy-path без регресса (чистая база)
**Условие:** shared-база чистая, self-deploy выполняет pull.
- **PASS:** поведение и exit-коды (0/1/2, ORCH-036) — байт-в-байт прежние (обычный fast-forward);
полный `pytest tests/ -q` зелёный.
- **FAIL:** изменилось наблюдаемое поведение/exit-код на чистой базе, либо красный регресс.
---
## AC-5 — Self-hosting safety
**Условие:** исполнение пути гигиены/деплоя.
- **PASS:** нет операций над веткой `main` на remote, нет force-push, нет рестарта прод-контейнера
вне штатного гейта, нет удаления worktree/веток других активных задач; операции строго в пределах
настроенного пути deploy-базы; тест/анализ это подтверждает.
- **FAIL:** любое из перечисленных нарушений присутствует или возможно.
---
## AC-6 — Kill-switch и обратимость
**Условие:** новый флаг выключен.
- **PASS:** деплой ведёт себя байт-в-байт как до ORCH-112 (голый `git pull origin main`); включение
флага активирует устойчивое поведение; область скоупится на self-hosting (прочие репо не затронуты).
- **FAIL:** выключенный флаг меняет поведение, либо нет kill-switch, либо затронуты прочие репо.
---
## AC-7 — Сходимость после cancel/failed
**Условие:** задача отменена (STOP/ORCH-090) или job завершился `failed`, оставив остатки в
рабочем дереве deploy-базы.
- **PASS:** shared-база сходится к чистому `origin/main` (через автоочистку в деплое и/или дворник),
и последующий self-deploy проходит без ручного вмешательства; покрыто интеграционным тестом.
- **FAIL:** остатки сохраняются и блокируют последующий деплой/git-операцию.
---
## AC-8 — Наблюдаемость
**Условие:** обнаружена грязная deploy-база.
- **PASS:** факт обнаружения и автоочистки (или отказ) — в логах (структурно) и в Telegram-алерте
(кликабельный номер); алерт best-effort, never-raise (его сбой не валит деплой).
- **FAIL:** тихая очистка без следа в логах/уведомлениях, либо сбой алерта роняет деплой.
---
## AC-9 — Инвариант конвейера и БД не тронуты
**Условие:** диф задачи.
- **PASS:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict
ключи / схема БД / exit-code-контракт хука — байт-в-байт; структурные тесты конвейера зелёные.
- **FAIL:** любой из перечисленных контрактов изменён.
---
## AC-10 — Документация (golden source)
**Условие:** изменён функционал deploy-базы/гигиены.
- **PASS:** обновлены `docs/operations/INFRA.md` (инвариант deploy-база ≠ workspace) и
`docs/architecture/README.md`; `CHANGELOG.md`/`CLAUDE.md` отражают изменение; ADR заведён на
стадии `architecture` (после эскалации `escalate: full-cycle`).
- **FAIL:** функционал изменён, доки/ADR не обновлены (reviewer → finding ≥P1, CLAUDE.md §6).
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1 |
| AC-2 | BR-1, BR-2 / FR-1, FR-2 |
| AC-3 | NFR-2 / FR-1 (ограничение) |
| AC-4 | BR-5 / FR-1 |
| AC-5 | NFR-1 / FR-1, FR-2 |
| AC-6 | NFR-3, NFR-6 / FR-5 |
| AC-7 | BR-2 / FR-2 |
| AC-8 | BR-4 / FR-4 |
| AC-9 | NFR-5 / FR-1…FR-5 |
| AC-10 | BR-3 / FR-3 |

View File

@@ -0,0 +1,83 @@
work_item: ORCH-112
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
title: "Гигиена shared deploy-базы: устойчивость self-deploy git pull к грязному дереву"
framework: pytest
scope: >
Покрывается: устойчивость self-deploy `git pull origin main` к грязной shared deploy-базе
(модифицированные tracked + untracked файлы), сходимость базы к чистому origin/main после
failed/cancelled задач, сохранность deploy-rollback-состояния, kill-switch/область, наблюдаемость,
self-hosting safety. Вне покрытия: модель worktree per-task (ORCH-2, не трогается), запрет ручных
операций оператора, изменения STAGE_TRANSITIONS/QG_CHECKS/схемы БД (их нет).
notes: >
TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс-тест воспроизведения инцидента ORCH-111: КРАСНЫЙ до фикса, ЗЕЛЁНЫЙ
после. Шелл-симуляции хука моделировать по образцу tests/test_deploy_hook_rollback_sim.py
(временный git-репо во временной директории, без сети/прода/ssh). Полный регресс `pytest tests/ -q`
обязан оставаться зелёным (NFR-5). Точные имена тест-модулей/функций уточнит разработчик; тип
гигиены (resilient-pull / janitor / guard) выберет архитектор — тесты сформулированы так, чтобы
проверять ТРЕБУЕМЫЙ ИНВАРИАНТ (база сходится к чистому origin/main, артефакты сохранены), а не
конкретный механизм.
tests:
- id: TC-01
type: integration
description: "РЕГРЕСС (обязательный, red→green): shared deploy-база с локальной модификацией tracked-файла src/config.py + untracked файлами — симуляция шага git pull хука приводит базу к чистому origin/main и НЕ падает с 'local changes would be overwritten by merge' (воспроизводит ORCH-111; красный до фикса)."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-02
type: integration
description: "Untracked WIP-файлы (install_lite.py / test_install_lite.py / lite-install.example.yaml) в shared-базе не блокируют операцию и база сходится к чистому origin/main."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-03
type: integration
description: "Сохранность (NFR-2): после гигиены файлы $REPO/.deploy-prev-image-* , deploy-hook.log, sibling .deploy-state-* / .merge-lease-*.json и .git/worktrees/* НЕ удалены; rollback по .deploy-prev-image-* остаётся работоспособным."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-04
type: integration
description: "Happy-path без регресса: на ЧИСТОЙ shared-базе шаг pull — обычный fast-forward; наблюдаемое поведение и exit-коды (0/1/2, ORCH-036) байт-в-байт прежние."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-05
type: unit
description: "Self-hosting safety: путь гигиены никогда не оперирует веткой main на remote, не делает force-push, не рестартит прод и не сносит worktree/ветки других задач; операции ограничены настроенным путём deploy-базы (статический/поведенческий ассерт)."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-06
type: unit
description: "Kill-switch off → деплой/pull байт-в-байт прежний (голый git pull origin main); on → активна устойчивая гигиена. Область applies(repo): self-hosting orchestrator real, прочие репо — no-op."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-07
type: integration
description: "Сходимость после cancel/failed: cancel_task (ORCH-090) / failed-исход не оставляет рабочих остатков в shared-базе, блокирующих будущий деплой; последующий self-deploy проходит без ручного вмешательства."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-08
type: unit
description: "Наблюдаемость: обнаружение грязной базы и факт автоочистки (или отказ) попадают в лог; Telegram-алерт best-effort/never-raise (его сбой не валит деплой), номер задачи кликабельный."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-09
type: unit
description: "Инвариант конвейера: STAGE_TRANSITIONS / реестр QG_CHECKS / имена и семантика check_* / machine-verdict ключи / exit-code-контракт хука — не изменены (структурный анти-регресс)."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS
- id: TC-10
type: unit
description: "Документация-инвариант: docs/operations/INFRA.md и docs/architecture/README.md содержат правило «shared main checkout — deploy/worktree-management база, не workspace» (структурная сверка)."
module: tests/test_deploy_checkout_hygiene.py
expected: PASS

View File

@@ -0,0 +1,244 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# ADR-001: Гигиена shared deploy-базы — устойчивый self-deploy `git pull` через resilient-pull в хуке
Work Item: **ORCH-112** — failed/cancelled task artifacts must be cleaned from shared checkout
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`** (решение
кросс-каттинговое: новый leaf-компонент на глобальном пути прод-деплоя + изменение прод-deploy-хука).
## Статус
Proposed
## Контекст
Self-deploy задачи **ORCH-111** упал на шаге `git pull origin main` хост-хука с
`error: Your local changes to the following files would be overwritten by merge: src/config.py`.
Деплой прерван → потребовал ручного вмешательства (на self-hosting это групповой риск — встаёт
деплой всех проектов). Грязь оставлена ранее **неуспешной/отменённой/перезапущенной** задачей
ORCH-104 в общем (shared) checkout.
Факты, сверенные с кодом:
1. **Точка отказа — голый pull.** `scripts/orchestrator-deploy-hook.sh:223-226` делает `cd "$REPO"`
(= `settings.deploy_host_repo_path` = main clone, строка 73) и **голый** `git pull origin main`
**без гигиены рабочего дерева**. Любая локальная правка tracked-файла блокирует merge. Во всём
`src/`+`scripts/` нет ни одного `git reset --hard` / `git clean` / `git stash` для приведения базы
к чистому состоянию.
2. **Инвариант «main clone ≠ workspace» соблюдён, но не задокументирован и не энфорснут.** Агенты
работают в worktree'ах `/repos/_wt/<repo>/<branch>` (`git_worktree.ensure_worktree`); `docker build`
берёт контекст **worktree** (`image_freshness._host_worktree_path`); fallback'и гейтов на main clone —
**только чтение** (`git show origin/main:...`). Значит грязь — почти наверняка ручной/брошенный WIP.
Но **нет механизма**, детектирующего/чистящего грязную базу, и **нет задокументированного
инварианта**.
3. **`cancel_task` чистит worktree + remote-ветку, но не shared checkout.** `stage_engine.cancel_task`
(строки 2333-2342): `remove_worktree(repo, branch)` + `gitea.delete_remote_branch(repo, branch)`
нулевое покрытие случая «грязная deploy-база».
4. **NFR-2 (сохранность) — жёсткое ограничение.** Сверено `.gitignore` + `src/config.py`:
- `.env`, `data/`, `*.db`, `.venv/`, `__pycache__/`, `build/`, `.env.staging`, `.env.watchdog`,
`deploy/bundled/repos/`**gitignored** → переживают `git clean -fd` (без `-x`) автоматически.
**Прод-секреты `.env` и БД `data/*.db` пропадут только при ошибочном `-x`.**
- `.deploy-prev-image-prod` (`deploy_prod_prev_image_file`) / `.deploy-prev-image-staging`,
а также `deploy-hook.log` (fallback-локация лога, хук строки 60-65) — **untracked, НЕ gitignored**
→ голый `git clean -fd` их **удалит** → сломает rollback (`do_rollback`, хук строки 107-139).
- Sibling-состояния `<repos_dir>/.deploy-state-<repo>/...` (`self_deploy._state_dir`) и
`<repos_dir>/.merge-lease-<repo>.json` (`merge_gate`, строка 315) лежат под **родителем** `$REPO`
(`settings.repos_dir`, не `$REPO`) → `git clean` в `$REPO` их физически не достаёт.
- `.git/worktrees/*` (админ-записи worktree) — внутри `.git/`, `git clean` его **никогда** не трогает.
«Как есть» не годится: deploy-база трактуется как «всегда чистая», что не гарантировано; устойчивость
должна быть на стороне системы — база обязана **самовосстанавливаться** в чистый `origin/main` перед pull.
## Решение
### Сводка
Сделать self-deploy `git pull` **устойчивым к грязной shared deploy-базе** через **resilient-pull,
встроенный в хук** (`--deploy`): перед `git pull origin main` добавляется детерминированный
hygiene-блок, который при обнаружении грязного дерева приводит базу к чистому актуальному `origin/main`
(`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`), **строго сохраняя**
rollback/лог-артефакты (NFR-2). Блок гейтится env-переменной `CHECKOUT_HYGIENE`, которую инжектит
`self_deploy.build_deploy_command` **только** когда новый leaf `src/checkout_hygiene.py`-`applies(repo)`
истинен (kill-switch + self-hosting-скоуп). Сходимость базы после failed/cancelled (FR-2) достигается
этим же deploy-time self-heal — **без** расширения `cancel_task` и **без** фонового «дворника».
Наблюдаемость — sentinel-файл от хука, который читает Phase-C finalizer и шлёт Telegram-алерт.
**Инвариант (NFR-5):** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` /
machine-verdict ключи (`deploy_status:`/`staging_status:`/`security_status:`) / схема БД /
exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт не тронуты**. Это устойчивость deploy-пути
и политика гигиены, **не** Quality Gate и **не** стадия.
### D1 — Механизм: resilient-pull в хуке (а не janitor / не container-side clean) — FR-1, AC-1/AC-2
Падающий `git pull` исполняется **на хосте** внутри detached-хука (`self_deploy.build_deploy_command`
→ ssh + setsid). Поэтому гигиена обязана выполняться **в самом хуке, на хосте, непосредственно перед
pull** — это исключает TOCTOU-гонку, которая возникла бы при чистке shared-mount из контейнера
параллельно бегущему detached-хуку.
Новый блок «2a. Resilient pull» в `scripts/orchestrator-deploy-hook.sh` **между** шагом «1. Capture
PREV_IMG» и шагом «2. Pull latest code», под `if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]`:
```bash
# 2a. ORCH-112: resilient pull — converge the shared deploy-base to a clean
# origin/main BEFORE the pull, so a dirty working tree (manual/abandoned WIP)
# never blocks the deploy. Gated by CHECKOUT_HYGIENE (Python kill-switch +
# self-hosting scope). NEVER `-x` (would delete .env/data/*.db); EXCLUDES the
# rollback/log artefacts (NFR-2). Best-effort: any failure is logged and the
# bare `git pull` below still runs (never-break).
if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]; then
dirty="$(git status --porcelain 2>/dev/null || true)"
if [[ -n "$dirty" ]]; then
log "HYGIENE: dirty deploy-base detected, converging to origin/main:"
log "$dirty"
git fetch origin main >> "$LOG" 2>&1 || log "HYGIENE: fetch failed (continuing)"
git reset --hard origin/main >> "$LOG" 2>&1 || log "HYGIENE: reset failed (continuing)"
git clean -fd \
-e '.deploy-prev-image-*' \
-e 'deploy-hook.log' \
>> "$LOG" 2>&1 || log "HYGIENE: clean failed (continuing)"
if [[ -n "${HYGIENE_REPORT:-}" ]]; then
{ printf 'dirty=1\n'; printf '%s\n' "$dirty"; } > "$HYGIENE_REPORT" 2>/dev/null || true
fi
else
log "HYGIENE: deploy-base already clean (no-op)"
fi
fi
```
- `git status --porcelain` детектирует грязь; **чистая база → блок no-op** (порожний вывод) → дальше
обычный `git pull` (AC-4, happy-path байт-в-байт).
- `git fetch origin main` обновляет `origin/main`-ref, чтобы `reset --hard origin/main` сходился к
**актуальному** источнику истины; tracked-правки (`src/config.py`) дискардятся (AC-1).
- `git clean -fd` снимает untracked-остатки (AC-2). **`-x` запрещён** (см. D2). Последующий
`git pull origin main` (строка 226, **не изменяется**) становится no-op fast-forward.
- Шаг 1 хука пишет `.deploy-prev-image-prod` **до** hygiene → exclude `-e '.deploy-prev-image-*'`
сохраняет rollback-снимок **этого** деплоя (AC-3).
### D2 — Сохранность deploy-состояния: NFR-2 как жёсткий контракт — AC-3
**INV-HYGIENE-1 (никогда `-x`).** Hygiene-`git clean` исполняется **только** как `git clean -fd`
(без `-x`). `-x` удалил бы gitignored `.env` (прод-секреты), `data/*.db` (БД прода), `build/` — что
катастрофично. Анти-регресс: статический тест ассертит, что hygiene-блок хука **не содержит** токена
`-x` (TC-05/TC-09).
**INV-HYGIENE-2 (явные excludes).** `.deploy-prev-image-*` и `deploy-hook.log` untracked-но-НЕ-ignored
→ обязательны `-e '.deploy-prev-image-*'` и `-e 'deploy-hook.log'`. TC-03 доказывает их неудаление и
работоспособность rollback после гигиены.
**INV-HYGIENE-3 (скоуп = `$REPO`).** Hygiene оперирует **только** рабочим деревом `$REPO`. Sibling
`.deploy-state-*` / `.merge-lease-*.json` (под `<repos_dir>`, родитель `$REPO`) и `.git/worktrees/*`
(внутри `.git/`) **вне** области `git clean` в `$REPO` — подтверждено топологией; гигиена их не достаёт.
### D3 — Гейтинг: leaf `src/checkout_hygiene.py` + инжекция env в `build_deploy_command` — FR-5, AC-6
Новый чистый never-raise leaf `src/checkout_hygiene.py` (по образцу `serial_gate`/`cancel`/`self_deploy`;
импортирует только `config`, лениво — `qg.checks.is_self_hosting_repo`):
- `applies(repo) -> bool``checkout_hygiene_enabled=False``False` (kill-switch, голый pull 1:1 до
ORCH-112); `checkout_hygiene_repos` (CSV) непуст → только перечисленные репо; **пусто →
self-hosting only** (`is_self_hosting_repo`, как `self_deploy_repos`). Локальный, без сети, **первым**.
- `hook_env(repo, work_item_id) -> str` — возвращает префикс `CHECKOUT_HYGIENE=1 HYGIENE_REPORT=<path>`
(через `shlex.quote`) **только** при `applies==True`; иначе `""`. `HYGIENE_REPORT` =
`os.path.join(self_deploy.host_state_dir(repo, work_item_id), "hygiene")` — в том же deploy-state
каталоге, что и `result` (shared mount, читается контейнером).
- `read_report(repo, work_item_id) -> dict | None` — читает sentinel `hygiene` через
`self_deploy.container_state_dir`; never-raise.
- `alert_dirty(...)` — best-effort `send_telegram` (кликабельный номер), never-raise (D5).
- `snapshot() -> dict` — read-only блок для `GET /queue` (флаг/скоуп; опционально).
`self_deploy.build_deploy_command` (строки 253-261) добавляет `checkout_hygiene.hook_env(repo, wi)` в
`env_assignments` (одна строка-присваивание в префиксе detached-команды; точно как ORCH-058
`EXPECTED_REVISION`). Когда `applies==False` → префикс пуст → хук видит `CHECKOUT_HYGIENE` неустановленным
→ блок 2a — no-op → **голый `git pull` (1:1 до ORCH-112)**.
Флаги в `src/config.py` (дефолт = боевое):
- `checkout_hygiene_enabled: bool = True` (env `ORCH_CHECKOUT_HYGIENE_ENABLED`);
- `checkout_hygiene_repos: str = ""` (env `ORCH_CHECKOUT_HYGIENE_REPOS`; пусто → self-hosting only).
### D4 — Сходимость после failed/cancelled: deploy-time self-heal достаточен; `cancel_task` НЕ расширяется — FR-2, AC-7
Нормальный конвейер **не пишет** в main clone (факт 2 контекста) → грязь — ручной/брошенный WIP.
Resilient-pull (D1) приводит базу к чистому `origin/main` **на следующем же self-deploy** → база
**сходится** независимо от источника остатков → FR-2/AC-7 удовлетворены deploy-time self-heal.
`stage_engine.cancel_task` (ORCH-090) **намеренно не расширяется** на shared-базу:
- избегаем касания критично-оконного каскада ORCH-090 (`cancel.in_critical_window`, adr-0026);
- избегаем container-side мутации deploy-базы (гонка с возможным параллельным деплоем);
- расширение было бы избыточным — self-heal в D1 уже гарантирует сходимость.
**Отвергнут активный «дворник» (janitor)** (фоновый актор, приводящий базу к чистоте по таймеру/событию):
новый background-поток + дополнительная гонка с detached-деплоем и держателем merge-lease, без выигрыша
сверх deploy-time self-heal.
### D5 — Наблюдаемость: sentinel хука → Telegram-алерт finalizer'а — FR-4, AC-8
Detached-хук (bash) не шлёт Telegram сам. Он пишет sentinel `hygiene` (`HYGIENE_REPORT`) в deploy-state
каталог. Phase-C finalizer (`stage_engine`, уже читает `result` через `self_deploy.read_result`) после
маппинга exit-code вызывает `checkout_hygiene.read_report` + `alert_dirty` (best-effort):
структурный лог + Telegram (`send_telegram`, кликабельный `ORCH-112` через `plane_issue_link`,
`disable_web_page_preview`). Сбой алерта **не валит** деплой (never-raise, AC-8). Опционально — блок
`checkout_hygiene` в `GET /queue`.
### D6 — Инвариант deploy-базы: документирование + de-facto энфорс — FR-3, AC-10
Инвариант «`<host_repos_dir>/<repo>` (main checkout) — deploy/worktree-management база, **НЕ**
редактируемый workspace; рабочие изменения туда не пишутся конвейером/агентами» документируется в
`docs/operations/INFRA.md` (топология + self-hosting) и `docs/architecture/README.md` (раздел ORCH-36).
**De-facto энфорс** — сам resilient-pull (D1): любая локальная правка дискардится при следующем деплое.
**Отвергнут** тяжёлый рантайм-guard на горячих путях (FR-3 «где осуществимо, без изменения горячих
путей») — он не нужен: self-heal уже обеспечивает сходимость, а guard добавил бы риск/латентность.
### D7 — Скоуп пути: только `--deploy` pull, не `--build-staging`
Hygiene врезается **только** в режим `--deploy` (где есть падающий `git pull`). Режим `--build-staging`
(хук строки 166-204) собирает образ из `BUILD_CONTEXT` = **worktree** и **не делает** `git pull`
hygiene там не нужна и не добавляется.
## Альтернативы
- **`git stash` вместо `reset --hard`** — отвергнуто: накапливает stash-записи в deploy-базе, не
«сходится к чистому origin/main», прячет источник грязи; сложнее и менее детерминирован.
- **Container-side Python clean перед ssh-dispatch** — отвергнуто (D1): TOCTOU-гонка с detached-хуком,
дублирование пути deploy-базы.
- **Активный фоновый janitor** — отвергнуто (D4): новый актор + гонки, нулевой выигрыш над self-heal.
- **Расширить `cancel_task` на shared-базу** — отвергнуто (D4): касание критично-оконного каскада
ORCH-090 + container-side мутация; избыточно.
- **Тяжёлый рантайм-guard инварианта** — отвергнуто (D6): не нужен при self-heal, риск на горячем пути.
- **`git clean -xfd`** — **категорически** отвергнуто (D2/INV-HYGIENE-1): удалит `.env`/`data/*.db`.
## Последствия
- **+** Self-deploy `git pull` устойчив к грязной shared-базе → нет ручного разруливания (BR-1, AC-1/2).
- **+** База **сходится** к чистому `origin/main` после failed/cancelled/брошенных задач (BR-2, AC-7).
- **+** Happy-path и kill-switch-off — байт-в-байт (BR-5, AC-4/AC-6); конвейер/БД/exit-коды не тронуты.
- **+** Наблюдаемость: оператор видит факт автоочистки (BR-4, AC-8); инвариант задокументирован (AC-10).
- **** `reset --hard` **дискардит** локальные правки deploy-базы безвозвратно. Митигейшн: это **ровно**
инвариант (база = `origin/main`, источник истины — remote; deploy лишь fast-forward'ит); скоуп —
self-hosting; наблюдаемость показывает, что именно сброшено.
- **** Новый leaf + хук-блок + флаги = площадь сопровождения. Митигейшн: leaf минимальный, образец
существующих leaf'ов; never-raise; полный набор анти-регресс-тестов (`04-test-plan.yaml`).
- **Откат:** `ORCH_CHECKOUT_HYGIENE_ENABLED=false` → деплой байт-в-байт до ORCH-112 (голый
`git pull origin main`); полный откат — revert leaf + хук-блок + флагов.
## Ссылки
- BRD: `docs/work-items/ORCH-112/01-brd.md`
- TRZ: `docs/work-items/ORCH-112/02-trz.md`
- Acceptance: `docs/work-items/ORCH-112/03-acceptance-criteria.md`
- Test-plan: `docs/work-items/ORCH-112/04-test-plan.yaml`
- Infra-requirements: `docs/work-items/ORCH-112/07-infra-requirements.md`
- Tech-risks: `docs/work-items/ORCH-112/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`
- Сверено по коду: `scripts/orchestrator-deploy-hook.sh:73,107-139,223-226`, `src/self_deploy.py:119-135,
220-278`, `src/merge_gate.py:315`, `src/config.py:285-291`, `.gitignore`, `src/stage_engine.py:2333-2342`
- Трассировка (CLAUDE.md §9 / ORCH-078): ORCH-036 (self-deploy, adr-0007), ORCH-058 (image-freshness,
adr-0008), ORCH-090 (cancel, adr-0026) — инварианты не нарушаются.

View File

@@ -0,0 +1,59 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 07 — Инфра-требования: ORCH-112 — гигиена shared deploy-базы
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: architecture
> When-applicable. Затрагивается **жизненный цикл shared deploy-базы** (`<host_repos_dir>/<repo>`),
> а не топология контейнеров/портов/томов. Контейнеры/сеть/тома — `N/A`.
## I-1. Топология / окружения
**Без изменений в составе.** Контейнеры (`orchestrator` 8500 / `orchestrator-staging` 8501), сеть
(`network_mode: host`), порты, тома — прежние.
Затрагивается **deploy-база** `<host_repos_dir>/<repo>` (= `/home/slin/repos/orchestrator` ==
`/repos/orchestrator` в контейнере через bind-mount == `settings.deploy_host_repo_path`). **Нормативно
закрепляется инвариант:** deploy-база — **deploy/worktree-management база, НЕ редактируемый workspace**.
Рабочие изменения туда не пишутся конвейером/агентами (агенты — worktree `/repos/_wt/<repo>/<branch>`,
build — worktree-контекст, fallback'и гейтов — read-only `git show origin/main`). Документируется в
`docs/operations/INFRA.md` (топология + self-hosting) и `docs/architecture/README.md` (раздел ORCH-36).
**Контракт сохранности рабочего дерева deploy-базы (NFR-2, жёсткий):** автоочистка hygiene
(`git clean -fd`, **без** `-x`) **обязана сохранять**:
| Артефакт | Расположение | Почему сохраняется |
|----------|--------------|--------------------|
| `.deploy-prev-image-prod` / `.deploy-prev-image-staging` | `$REPO/` (untracked, НЕ ignored) | rollback-снимок → `-e '.deploy-prev-image-*'` |
| `deploy-hook.log` | `$REPO/` (fallback-лог) | аудит → `-e 'deploy-hook.log'` |
| `.env`, `data/`, `*.db`, `build/` | `$REPO/` (gitignored) | прод-секреты/БД → переживают `git clean` **без** `-x` |
| `.deploy-state-<repo>/*`, `.merge-lease-<repo>.json` | `<repos_dir>/` (sibling, родитель `$REPO`) | вне области `git clean` в `$REPO` |
| `.git/worktrees/*` | `$REPO/.git/` | `git clean` никогда не трогает `.git/` |
## I-2. Переменные окружения / секреты
Две новые env-переменные (`src/config.py`, дефолт = боевое; **обновить `.env.example`**):
| Ключ | Env | Дефолт | Назначение |
|------|-----|--------|-----------|
| `checkout_hygiene_enabled` | `ORCH_CHECKOUT_HYGIENE_ENABLED` | `True` | kill-switch resilient-pull; `False` → голый `git pull` (1:1 до ORCH-112) |
| `checkout_hygiene_repos` | `ORCH_CHECKOUT_HYGIENE_REPOS` | `""` | CSV-скоуп; пусто → self-hosting only (`orchestrator`) |
Внутренние env, инжектируемые в detached-хук `self_deploy.build_deploy_command` (не операторские):
`CHECKOUT_HYGIENE=1`, `HYGIENE_REPORT=<host_state_dir>/hygiene`. Новых секретов нет.
## I-3. Деплой / рестарт
**Рестарт прод-контейнера задачей ORCH-112 — НЕ требуется и ЗАПРЕЩЁН** (self-hosting инвариант,
CLAUDE.md). Изменение активируется штатно: новый промпт/хук/код `cat`-ается/деплоится в обычном
self-deploy-цикле через **staging-гейт (8501)** сначала, затем `Confirm Deploy` (ORCH-059). Хук-блок
hygiene исполняется **только** в `--deploy` режиме (где есть `git pull`); `--build-staging` собирает из
worktree и не пуллит → не затронут. Exit-code-контракт хука (0/1/2, ORCH-036) — байт-в-байт.
## I-4. CI/CD
**Без изменений** `.gitea/workflows/`. Новый тест-модуль `tests/test_deploy_checkout_hygiene.py`
(шелл-симуляция хука во временном git-репо, без сети/прода/ssh — образец
`tests/test_deploy_hook_rollback_sim.py`) исполняется обычным `pytest tests/ -q`. Полный регресс обязан
оставаться зелёным (NFR-5).

View File

@@ -0,0 +1,42 @@
---
work_item: ORCH-112
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-112 — гигиена shared deploy-базы
Work Item: **ORCH-112** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Риски реализации resilient-pull и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **`git clean -x`** ошибочно добавлен → удалит gitignored `.env` (прод-секреты) / `data/*.db` (БД прода) / `build/` — катастрофа на прод-deploy-базе | Низ. | **Выс.** | INV-HYGIENE-1 (ADR D2): только `git clean -fd`, **никогда** `-x`. Статический анти-регресс-тест ассертит отсутствие `-x` в hygiene-блоке хука (TC-05/TC-09); явная рамка в ADR + INFRA |
| TR-2 | Неверный/отсутствующий `-e` exclude → `git clean -fd` удалит `.deploy-prev-image-*` → сломан rollback (`do_rollback`) | Сред. | **Выс.** | INV-HYGIENE-2: обязательные `-e '.deploy-prev-image-*'` + `-e 'deploy-hook.log'`; TC-03 доказывает неудаление и работоспособность rollback; шаг 1 хука пишет prev-image **до** hygiene |
| TR-3 | Сбой шага hygiene (fetch/reset/clean) маскирует/ухудшает исход деплоя | Низ. | Сред. | never-break (ADR D1): каждая git-операция `\|\| log "...continuing"`; деплой продолжается к голому `git pull`; sentinel-отчёт фиксирует факт; fail-safe, исход не хуже текущего |
| TR-4 | `reset --hard origin/main` безвозвратно дискардит локальные правки deploy-базы, которые кто-то «нужными» считал | Низ. | Сред. | Это **ровно** инвариант: deploy-база = `origin/main` (источник истины — remote); deploy лишь fast-forward'ит. Наблюдаемость (D5) показывает сброшенное; скоуп — self-hosting; документировано (INFRA/README) |
| TR-5 | Гонка: hygiene на хосте чистит mount, пока контейнер читает deploy-базу | Низ. | Низ. | Деплой сериализован (serial-gate ORCH-088, один деплой за раз); hygiene в detached-хуке непосредственно перед pull; нормальный конвейер deploy-базу не читает в этот момент (worktree-изоляция). TOCTOU между `status` и `reset` пренебрежимо (один процесс) |
| TR-6 | Скоуп-leak: hygiene затронет прочие репо / синхронный деплой агентом | Низ. | Сред. | `applies(repo)` (локально, первым): пусто → self-hosting only; инжекция env только на self-deploy-пути (сам self-hosting-скоупленный, `self_deploy_applies`); TC-06 |
| TR-7 | Стейл `origin/main` (fetch не выполнился) → `reset --hard` сходится к устаревшему коммиту | Низ. | Низ. | `git fetch origin main` перед reset; при сбое fetch — `\|\| log` + продолжение; последующий `git pull origin main` (строка 226, не тронута) до-тянет недостающее; deploy промоутит build-once `SOURCE_IMAGE` (артефакт не зависит от дерева main clone) |
| TR-8 | Расширение объёма (касание `cancel_task`/`STAGE_TRANSITIONS`/QG) при реализации | Низ. | Сред. | ADR D4 явно запрещает расширение `cancel_task`; NFR-5 байт-в-байт; TC-09 структурно ассертит неизменность контрактов; трассировка ORCH-036/058/090 |
## Сводный вывод
Доминирующий класс — **деструктивная гигиена рядом с прод-rollback-состоянием** (TR-1/TR-2): низкая
вероятность, высокое влияние, **полностью** снимается контрактом сохранности (INV-HYGIENE-1/2) + явными
тестами (TC-03/TC-05/TC-09). Изменение аддитивно, под kill-switch (`checkout_hygiene_enabled`, дефолт
`True`; off → байт-в-байт до ORCH-112), never-raise, self-hosting-скоупленное, **не трогает**
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схему БД/exit-code-контракт хука.
**Эскалация:** вводится новый leaf-компонент (`src/checkout_hygiene.py`) на глобальном пути прод-деплоя
+ правка прод-deploy-хука + сквозной ADR adr-0044 → рекомендуется лейбл **`arch:major-change`** для
reviewer-внимания (изменение safety-critical, в прод-deploy-пути). Возврат в анализ **не** требуется:
ТЗ удовлетворяется без нарушения принципов архитектуры (всё в Docker на одном сервере, без новых
зависимостей/очередей/k8s, без рестарта прода вне staging-гейта). Остаточный риск для прод-конвейера —
**низкий** при соблюдении INV-HYGIENE-1/2/3.

View File

@@ -0,0 +1,123 @@
---
verdict: APPROVED # APPROVED | REQUEST_CHANGES — строго одно из двух, UPPERCASE
work_item: ORCH-112
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-15
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-112
version: 1
---
# Review ORCH-112 — deploy-base checkout-hygiene (resilient-pull)
## Summary
Багфикс инцидента **ORCH-111** (bug → escalate full-cycle): прод-self-deploy падал на голом
`git pull origin main` хост-хука из-за грязного shared main checkout (остатки ORCH-104 от ORCH-104).
Реализован **resilient-pull в хуке** (`--deploy`): перед pull при обнаружении грязи база сводится к
чистому `origin/main` (`git fetch` + `git reset --hard origin/main` + скоупленный `git clean -fd`),
под kill-switch, never-raise, скоуп self-hosting.
Проверены все 4 оси. Реализация **точно соответствует** ADR-001 (D1D7) и сквозному adr-0044, все
10 критериев приёмки (AC-1…AC-10) покрыты содержательными тестами, документация (golden source)
обновлена в том же PR, инварианты конвейера/БД/exit-code-контракт хука — байт-в-байт не тронуты.
**Вердикт: APPROVED.** P0/P1/P2 findings отсутствуют.
### Что сверено (доказательная база)
**Ось 1 — соответствие ТЗ (02-trz) / критериям (03-acceptance-criteria):**
- FR-1 (устойчивый pull) / AC-1 — ✅ хук-блок «2a. Resilient pull» + регресс **TC-01** (зелёный после
фикса) и **TC-01b** (тот же грязный base без гигиены → `would be overwritten by merge`, репро инцидента).
- FR-1 / AC-2 (untracked WIP) — ✅ **TC-02** (остатки сняты, не протекают в деплой).
- NFR-2 / AC-3 (сохранность) — ✅ **TC-03** (`.deploy-prev-image-*`, `deploy-hook.log`, gitignored
`.env`/`data/*.db`, sibling `.deploy-state-*`/`.merge-lease-*.json`, `.git/worktrees/*` — на месте) +
**TC-05** статический контракт (`git clean -fd`, **никогда `-x`**, явные excludes).
- BR-5 / AC-4 (happy-path) — ✅ **TC-04** (чистая база → no-op + fast-forward, exit-коды байт-в-байт).
- NFR-1 / AC-5 (self-hosting safety) — ✅ скоуп `$REPO`, `reset --hard origin/main` (не локальная
догадка), нет push/force-push (TC-05 ассерт).
- FR-5 / AC-6 (kill-switch + обратимость) — ✅ **TC-06** (off → инертно; пустой CSV → self-hosting only;
enduro не затронут).
- FR-2 / AC-7 (сходимость после cancel/failed) — ✅ **TC-07** (deploy-time self-heal; `cancel_task`
корректно НЕ расширён — D4).
- FR-4 / AC-8 (наблюдаемость) — ✅ **TC-08** (`read_report`/`alert_dirty` never-raise) + врезка в
`run_deploy_finalizer` (sentinel → Telegram, best-effort).
- NFR-5 / AC-9 (инвариант конвейера/БД) — ✅ **TC-09** + проверка дифа: `STAGE_TRANSITIONS`/`QG_CHECKS`/
`check_*`/machine-verdict/схема БД/exit-code-контракт хука (0/1/2) не тронуты.
- BR-3 / AC-10 (документация) — ✅ см. ось 4.
**Ось 2 — соответствие ADR:**
- ADR-001 D1D7 реализованы дословно: resilient-pull в хуке (не janitor/не container-side, D1),
NEVER `-x` + excludes (D2), leaf `checkout_hygiene.py` + инжекция env в `build_deploy_command` (D3),
`cancel_task` не расширяется / janitor не вводится (D4), sentinel → finalizer-alert (D5), docs (D6),
только `--deploy` не `--build-staging` (D7, подтверждено размещением блока между шагами 1 и 2 пути
`--deploy`).
- Трассировка (ORCH-078): правка `build_deploy_command` (маркеры ORCH-101/ORCH-058) — чисто аддитивна
(одно env-присваивание после `EXPECTED_REVISION`), инвариант image-freshness не сломан; ORCH-036
exit-code-контракт и ORCH-090 cancel-каскад не нарушены; INV-4 (никогда push/force-push `main`)
соблюдён.
**Ось 3 — качество кода:**
- Leaf чистый, never-raise, ленивые импорты (`self_deploy`/`qg.checks`/`notifications`) — leaf-инвариант
доказан **TC-05** (`leaf_is_a_pure_leaf`). Docstrings на всех публичных функциях. `shlex.quote` на
инжектируемом пути. Env-проводка консистентна с существующим паттерном `result`-sentinel
(`initiate_deploy` пред-создаёт `container_state_dir` → запись `hygiene` гарантированно проходит).
- **Багфикс-трек регресс-тест (ORCH-019 / BR-4):** присутствует — **TC-01** (фиксатор дефекта,
зелёный после фикса) в паре с **TC-01b** (репродукция инцидента: голый pull аборт без гигиены).
- Все ссылки на API существуют (`notifications.link_for`/`send_telegram`, `self_deploy.host_state_dir`/
`container_state_dir`, `qg.checks.is_self_hosting_repo`). `repo`/`work_item_id`/`task_id` в скоупе
финализатора.
- Тесты содержательные: 17 TC (шелл-симуляция реального хука в герметичном git-репо без сети/прода/ssh
+ unit). Прогон: **17/17 зелёные**; смежные deploy/config/stage_engine/frontmatter — **200/200 зелёные**;
docs/hardcode/canon — **101/101 зелёные**.
**Ось 4 — документация (golden source):**
- `src/` изменён → документация обновлена **в том же PR**: `docs/operations/INFRA.md` (инвариант
deploy-база ≠ workspace), `docs/architecture/README.md` (раздел ORCH-112 design), `CHANGELOG.md`,
`CLAUDE.md` (паспортный блок), `.env.example` (новые ключи), ADR-001 + сквозной
`docs/architecture/adr/adr-0044`. Консистентны между собой и с кодом.
- Обзорные доки (ORCH-079): открытые пункты `README.md` «Известные ограничения» (Telegram 48h /
intra-repo deps / batch-автоном) этим PR **не закрываются** → обновление не требуется. ✅
- Витрина системы (ORCH-011): фикс — внутренняя устойчивость deploy-пути, **не** новая
стадия/гейт/агент/интеграция/способность → витрина `docs/overview/` не затронута;
`tests/test_system_docs.py` зелёный. ✅
## Findings
### P0 — Blocker
- Нет.
### P1 — Must fix
- Нет.
### P2 — Should fix
- Нет.
### P3 — Nice-to-have (не блокирует, на усмотрение)
- `tests/test_deploy_checkout_hygiene.py::test_tc05_hook_clean_is_never_destructive` ассертит
`assert "-x" not in code` по **всем** исполняемым строкам хука. Текущий хук токена `-x` не содержит
(тест зелёный), но будущая легитимная конструкция (`set -x`, `[ -x file ]`, `chmod +x`) ложно уронит
ассерт. Можно сузить проверку до строки(ок) `git clean` — но это страховка критичного инварианта
INV-HYGIENE-1, поэтому строгость намеренна и допустима. Не блокирует.
## Документация
**Статус: обновлена полностью, в том же PR (golden source соблюдён).**
| Документ | Статус |
|----------|--------|
| `docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md` | ✅ заведён (architecture, после escalate full-cycle) |
| `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md` | ✅ сквозной ADR заведён |
| `docs/operations/INFRA.md` | ✅ инвариант deploy-база ≠ workspace + страховка resilient-pull |
| `docs/architecture/README.md` | ✅ раздел ORCH-112 (design) |
| `CHANGELOG.md` | ✅ запись [Unreleased] |
| `CLAUDE.md` | ✅ паспортный блок |
| `.env.example` | ✅ `ORCH_CHECKOUT_HYGIENE_ENABLED` / `_REPOS` |
| `docs/overview/` (витрина, ORCH-011) | не требуется (внутренний deploy-fix, не новая способность) |
| `README.md` «Известные ограничения» (ORCH-079) | не требуется (открытые пункты не закрываются) |
Необновлённой документации при изменённом `src/` **нет** → ось 4 пройдена, P0 по документации
отсутствует.

View File

@@ -0,0 +1,71 @@
---
result: PASS # PASS | FAIL — машинный вердикт, UPPERCASE
work_item: ORCH-112
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-15
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-112
---
# Test Report — ORCH-112
Гигиена shared deploy-базы: устойчивость self-deploy `git pull` к грязному дереву
(багфикс инцидента ORCH-111). Review-вердикт: **APPROVED** (`12-review.md`).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3 (plugins: cov-5.0.0, anyio-4.13.0, asyncio-0.23.8)
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-112-bug-failed-cancelled-task-arti/`
- Ветка: `feature/ORCH-112-bug-failed-cancelled-task-arti`
- Дата: 2026-06-15
## Smoke API (read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
| `GET /status` | PASS — задача 102 ORCH-112 на стадии `testing`, ветка совпадает |
| `GET /queue` | PASS — блок `serial_gate` присутствует (ORCH-088); `auto_labels` присутствует |
Блок `checkout_hygiene`/`serial_gate`/`auto_labels` — все на месте в полезной нагрузке `/queue`,
регресса смока нет.
## Покрытие ТЗ (TC из 04-test-plan.yaml ↔ 03-acceptance-criteria)
| TC ID | Описание | AC | Тест | Результат |
|-------|----------|----|------|-----------|
| TC-01 | Регресс ORCH-111: грязный tracked `src/config.py` + untracked → база сходится к чистому `origin/main`, pull не падает (red→green) | AC-1 | `test_tc01_dirty_tracked_edit_converges_and_deploys` (+ `test_tc01b_bare_pull_aborts_without_hygiene_documents_incident`) | PASS |
| TC-02 | Untracked WIP-файлы не блокируют и не протекают в деплой | AC-2 | `test_tc02_untracked_wip_does_not_block` | PASS |
| TC-03 | Сохранность `.deploy-prev-image-*`/`deploy-hook.log`/sibling `.deploy-state-*`/`.merge-lease-*.json`/`.git/worktrees/*` (NFR-2) | AC-3 | `test_tc03_preserves_rollback_and_sibling_artifacts` | PASS |
| TC-04 | Happy-path: чистая база → fast-forward, exit-коды байт-в-байт | AC-4 | `test_tc04_clean_base_fast_forwards_no_op_hygiene` | PASS |
| TC-05 | Self-hosting safety: нет операций над `main`/force-push/рестарта прода; `git clean -fd` (никогда `-x`); leaf чист | AC-5 | `test_tc05_hook_clean_is_never_destructive`, `test_tc05_leaf_is_a_pure_leaf` | PASS |
| TC-06 | Kill-switch off → инертно; пустой CSV → self-hosting only; скоуп репо | AC-6 | `test_tc06_kill_switch_off_is_inert`, `test_tc06_empty_csv_is_self_hosting_only`, `test_tc06_csv_scope_limits_repos` | PASS |
| TC-07 | Сходимость после cancel/failed → следующий self-deploy чист | AC-7 | `test_tc07_convergence_then_next_deploy_is_clean` | PASS |
| TC-08 | Наблюдаемость: `read_report`/`alert_dirty`, Telegram best-effort/never-raise | AC-8 | `test_tc08_read_report_none_when_absent`, `test_tc08_read_report_parses_dirty_sentinel`, `test_tc08_alert_dirty_never_raises_on_send_failure` | PASS |
| TC-09 | Инвариант конвейера: `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/exit-code-контракт хука не тронуты | AC-9 | `test_tc09_pipeline_contracts_untouched`, `test_tc09_hook_exit_code_contract_intact` | PASS |
| TC-10 | Документация-инвариант: INFRA.md и architecture/README.md содержат правило «main checkout — deploy-база, не workspace» | AC-10 | `test_tc10_docs_state_deploy_base_invariant` | PASS |
Каждый TC из `04-test-plan.yaml` выполнен и сопоставлен с критерием приёмки `03-acceptance-criteria.md`.
TC-01 (обязательный red→green регресс инцидента ORCH-111) — зелёный; парный TC-01b документирует
аборт голого pull без гигиены.
## Вывод pytest
### Целевой модуль `tests/test_deploy_checkout_hygiene.py`
```
collected 17 items
... 17 passed, 1 warning in 7.51s
```
### Полный регресс `pytest tests/ -q`
```
2018 passed, 1 warning in 342.01s (0:05:42)
```
(единственный warning — Pydantic V2 deprecation в `src/config.py:8`, существующий, не связан с задачей)
## Итог
PASS — все 10 TC (17 тест-функций) зелёные, полный регресс 2018/2018 зелёный, smoke API OK
(`/health`, `/status`, `/queue` с блоками `serial_gate` и `auto_labels`). Задача готова к переходу
на `deploy-staging`.

View File

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

View File

@@ -0,0 +1,38 @@
---
staging_status: SUCCESS
work_item: ORCH-112
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-15
model_used: claude-opus-4-8
timestamp: 2026-06-15T12:11:26Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live staging environment (`orchestrator-staging`,
port 8501), executed inside the container via the canonical path
`/repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
(ORCH-048: B6 registry-isolation reads `.env.staging` from the running instance's own process-env).
**Result: 8/10 checks PASS — exit code 0 → SUCCESS.**
- REAL failed: none
- SANDBOX_INFRA failed (waived, ORCH-061): C9a, C9b
The two failing checks (C9a "Branch appears in orchestrator-sandbox", C9b "Analyst job enqueued in
staging queue") are the known sandbox-infra checks that depend on SANDBOX bot accounts being project
members — not on the pipeline. With every REAL check green, the suite waives them and exits 0.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
Block A (SMOKE): A1 /health 200, A2 /queue 200, A3 ORCH_STAGING=true — all PASS.
Block B (ACCESS): B4 Plane sandbox, B5 Gitea push, B6 registry isolation — all PASS.
Block C (E2E, stub): C7 create issue PASS, C8 trigger pipeline PASS, C9a/C9b waived sandbox-infra.
Staging gate passed. Task may advance to the `deploy` stage.

View File

@@ -0,0 +1,7 @@
# Business Request: BUG: job-reaper must not re-run deploy-staging finalization while original finalizer is alive
Work Item ID: ORCH-113
## Description
TBD

View File

@@ -0,0 +1,167 @@
---
work_item: ORCH-113
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 01 — BRD (бизнес-требования): ORCH-113 — BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: analysis
> **Багфикс-трек → эскалация в полный цикл (`escalate: full-cycle`).** Задача помечена `Bug`, но
> сама баг-карточка явно требует «анализ контракта reaper, статуса `running/finalizing`, длительности
> grace и идемпотентности edge-гейтов» (см. «Ограничение» в бизнес-запросе) — это решение с
> несколькими проектными альтернативами (liveness-heartbeat finalizer'а / явный sub-state
> `finalizing` / per-stage grace / ownership-lease на edge-гейты) и нетривиальными инвариантами
> self-hosting, затрагивающее **задокументированный сквозной инвариант ORCH-065** (контракт
> живости reaper, `adr-0011`). По правилу ORCH-019 (ADR-001 D5) выпускается **полный** analysis-пакет,
> а трек эскалируется (`POST /bug-fast-track/escalate?work_item=ORCH-113`) → задача проходит стадию
> `architecture`. Прецедент — родственные задачи того же инцидент-кластера: ORCH-110 / ORCH-111
> («bug → escalate full-cycle»).
---
## 1. Бизнес-контекст и проблема
Оркестратор — self-hosting инструмент: его прод-контейнер обслуживает конвейер **всех** проектов из
одного инстанса с общей БД и общей очередью и дорабатывает сам себя. Фоновый демон **job-reaper**
(`src/job_reaper.py`, ORCH-065) — страховочный слой: он добивает «зомби»-job'ы, чей монитор умер,
не записав терминальный статус. Его Tier-2-ветка (процесс агента завершился — `agent_runs.exit_code`
записан, — но job всё ещё `running`) **неоднозначна**: это одновременно «монитор умер посреди
финализации» И «живой монитор ещё финализирует». Reaper разрешает неоднозначность таймером —
**finalization grace** `reaper_finalize_grace_s = 300` (db.py:1345-1348, job_reaper.py:36-44): если
`exit_code` записан дольше grace — трактует ситуацию как **мёртвый монитор** и сам до-водит стадию.
**Корневая ошибка контракта:** grace=300с построен на задокументированном допущении, что после записи
`finished_at` монитор делает лишь «git commit/push (+PR), БАГ-8-проверку и сетевые Plane-комментарии —
**секунды…десятки секунд**, и ТОЛЬКО ПОТОМ `_try_advance_stage`». Для ребра `deploy-staging → deploy`
это **неверно**: `_try_advance_stage` (`launcher._monitor_agent`, строка 998) синхронно, в потоке
монитора, прогоняет **весь набор тяжёлых детерминированных edge-под-гейтов**
`security``merge-gate` (полный локальный re-test, бюджет `merge_retest_timeout_s = 900`) →
`coverage` (ещё один полный `pytest --cov`) → `image-freshness` (docker-rebuild + пересоздание
staging). Это **минуты**, и весь этот объём идёт **ПОСЛЕ** штампа `finished_at` (launcher:861) и
**ДО** `_finalize_job` (launcher:1003). `finished_age_s` reaper меряет именно от `finished_at`
(db.py:1360) — то есть от **начала** финализации. Итог: пока живой монитор ещё внутри edge-гейтов,
reaper по истечении 300с считает его мёртвым и **независимо запускает тот же тяжёлый набор edge-гейтов
повторно**.
**Установленные факты инцидента (ORCH-111, deployer job `1914`, run_id `683`):**
- deployer завершился `exit_code=0` и записал `15-staging-log.md` со `staging_status: SUCCESS`;
- после выхода агента job `1914` оставалась `running`, т.к. монитор ещё финализировал стадию;
- финализация `deploy-staging` заняла **дольше 300с** (полный локальный re-test + coverage);
- reaper после `reaper_finalize_grace_s=300` трактовал это как умерший monitor и повторно вызвал
gate-driven advance (`_reap_exit0``_gate_driven_advance``_try_advance_stage``advance_stage`);
- edge-гейты `deploy-staging` исполнились **повторно**: `security-gate`, `merge-gate`, rebase, полный
локальный re-test;
- один из повторных re-test стал **красным**: `3 failed, 1916 passed, 1 warning, 14 errors in 444.79s`;
- задача была откатана `deploy-staging → development` (+ ложный developer-retry), хотя **параллельно**
исходный prod-deploy/finalizer дошёл до `deploy_status: SUCCESS`, **PR #130 был смержен**, задача
перешла `deploy → done`.
**Симптом:** две ветки системы расходятся по состоянию **одной** задачи — одна повторно откатывает
`deploy-staging`, другая успешно завершает deploy. Гонка + ложный rollback + ложный developer-retry +
шумные алерты + несогласованное состояние Plane/БД.
**Почему существующие гарды reaper не спасли:** атомарный claim-before-act
(`reap_running_job(... WHERE status='running')`, job_reaper.py:280) защищает **строку job** от
двойного терминального флипа, но **не защищает побочное исполнение edge-гейтов**: reaper вызывает
`_gate_driven_advance → advance_stage`, который и прогоняет тяжёлые под-гейты, **до/независимо** от
монитора. Гонка — в **side-effectful исполнении edge-гейтов**, а не в флипе строки. Дешёвая
read-only пред-проверка `_gate_is_green('deploy-staging')` читает лишь `check_staging_status`
(frontmatter `15-staging-log.md` = `SUCCESS`, зелёный) → reaper уверенно идёт в тяжёлый advance.
Tier-3 backstop (`reaper_max_running_s = 5400`) при этом не срабатывает — баг чисто в Tier-2 grace.
## 2. Объём (scope)
### В объёме
- Reaper **не должен** повторно исполнять тяжёлую финализацию `deploy-staging`/merge-gate (security /
merge-gate / локальный re-test / coverage / image-freshness), пока исходный monitor/finalizer ещё
**жив** или пока edge-гейты для этого job/stage **уже исполняются**.
- Повторная обработка завершившегося-но-ещё-`running` job на `deploy-staging` должна быть
**идемпотентной**: без второго локального re-test/merge-gate для того же job/stage без **строгого
владения состоянием**.
- Согласование Tier-2 grace (`reaper_finalize_grace_s`) с **фактической** wall-clock-длительностью
финализации `deploy-staging` ИЛИ замена таймерного критерия живости на сигнал, переживающий
«долгую, но живую» финализацию.
- Сохранение основной функции reaper (ORCH-065): реально **мёртвый** finalizer на `deploy-staging`
по-прежнему добивается за ограниченное время.
### Вне объёма
- Изменение `STAGE_TRANSITIONS` / `QG_CHECKS` / семантики любого `check_*` / machine-verdict ключей /
схемы существующих таблиц (правки — только аддитивные).
- Инфра-толерантность merge-gate к таймауту re-test и tree-kill осиротевших pytest-процессов — это
**ORCH-110** (союзная задача того же инцидента; не дублировать).
- Починка конкретных «мигающих» тестов, давших `3 failed … 14 errors`.
- Полный редизайн reaper или модели финализации монитора.
- **Выбор механизма** решения (heartbeat / sub-state `finalizing` / per-stage grace / ownership-lease)
— это **архитектурное решение** (06-adr), не зона аналитика.
## 3. Заинтересованные стороны
- **Owner / Слава** — заказчик исправления, держатель инвариантов self-hosting.
- **Конвейер всех проектов** (orchestrator self-hosting + enduro-trails) — общий инстанс/БД/очередь:
ложный rollback и гонка состояния касаются стабильности платформы в целом.
- **Операторы** — получатели алертов; именно их будят ложные «merge-gate FAILED / rolled back».
- **Архитектор** — принимает решение по механизму владения/живости (06-adr) после эскалации.
## 4. Бизнес-требования (BR)
- **BR-1** — Reaper **не должен** запускать второй прогон edge-гейтов ребра `deploy-staging → deploy`
(security / merge-gate / re-test / coverage / image-freshness) для job, чей исходный
monitor/finalizer **ещё жив**.
- **BR-2** — Повторная обработка завершившегося-но-`running` job на `deploy-staging` **идемпотентна**:
не более **одного** локального re-test/merge-gate на пару (job, stage) без строгого владения
состоянием; второй актор, не владеющий состоянием, **не исполняет** побочных шагов.
- **BR-3** — Критерий живости Tier-2 должен учитывать **реальную** wall-clock-длительность
финализации `deploy-staging` (включающую полный набор edge-гейтов), ИЛИ живость должна определяться
сигналом, который **переживает** долгую-но-живую финализацию (не одним `finished_age_s`).
- **BR-4** — Реально **мёртвый** монитор (краш посреди финализации `deploy-staging`) по-прежнему
должен добиваться reaper'ом за ограниченное время — основная функция ORCH-065 **сохраняется**;
фикс не превращает reaper в no-op для `deploy-staging`.
- **BR-5** — После согласования у задачи — **единственное** консистентное состояние: **никакого**
ложного отката `deploy-staging → development` и **никакого** ложного developer-retry после
фактически успешного deploy; ветки системы сходятся, не расходятся.
## 5. Нефункциональные требования (NFR)
- **NFR-1** — Контракт reaper сохранён: **never-raise** на единицу работы, **kill-switch**,
fail-safe; reaper остаётся наблюдателем-страховкой, не Quality Gate'ом.
- **NFR-2** — `STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict ключи / схема
существующих таблиц — **байт-в-байт**; любые БД-правки — только **аддитивные** (`_ensure_column` /
`CREATE TABLE IF NOT EXISTS`).
- **NFR-3** — Self-hosting-безопасно: фикс **никогда** не рестартит/не роняет прод-контейнер и
**никогда** не пушит/force-push'ит `main`.
- **NFR-4** — Обратная совместимость и обратимость: поведение reaper для **не-`deploy-staging`** стадий
и путь добивания **мёртвого** монитора сохранены; выключенный kill-switch → строго прежнее
поведение; раскат обратим.
- **NFR-5** — Restart-safe: in-memory состояние reaper сбрасывается при рестарте (это покрыто
стартовым `requeue_running_jobs`); любой **новый** маркер владения/живости должен быть либо durable,
либо безопасно восстановимым после рестарта.
- **NFR-6** — Сквозной инвариант ORCH-065/109/110 сохранён: `reaper_max_running_s (5400) >
Σ(deploy-staging gate-work) + grace` (Tier-3 backstop). Любая правка grace/таймаутов не должна его
нарушить.
## 6. Допущения и ограничения
- Задача помечена `Bug`; ввиду архитектурной природы — **эскалация в полный цикл** (нужен ADR +
анализ тех-рисков архитектором: 06-adr / 07 / 08 / 10).
- Инстанс общий для всех проектов (общая БД/очередь) — фикс не должен вносить регрессию для
enduro-trails и не-self репо.
- Выбор конкретного механизма владения/живости — за архитектором; настоящий BRD фиксирует **требования
и инварианты**, а не реализацию.
- Источник истины о «жив ли finalizer» сегодня отсутствует: pid агента в Tier-2 **уже мёртв** в обоих
случаях (`proc.wait()` вернулся), а живости **потока-монитора/финализатора** система не наблюдает —
это и есть пробел, который закрывает фикс.
## 7. Критерии успеха
Reaper при живом finalizer'е `deploy-staging` не запускает второй прогон edge-гейтов и не откатывает
задачу; повторная обработка идемпотентна; мёртвый finalizer по-прежнему добивается; после фикса нет
ложного rollback/developer-retry и расхождения состояния; инварианты ORCH-065/NFR-2 целы; полный
регресс `tests/` зелёный. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
## 8. Риски
- **Гонка/расхождение состояния** (наблюдалось): повторный откат после успешного deploy. **Высокий.**
- **Над-толерантность**: слишком «доверять живости» → реально мёртвый finalizer не добивается (регресс
ORCH-065). Сдерживается BR-4 + Tier-3 backstop.
- **Нарушение сквозного бюджета** при правке grace/таймаутов (NFR-6).
Детальная проработка и контрмеры — `10-tech-risks.md` (заполняет архитектор).

View File

@@ -0,0 +1,106 @@
---
work_item: ORCH-113
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 02 — ТЗ (TRZ): ORCH-113 — BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **требования к изменению**, выведенные из BRD и фактического кода. Конкретный
> механизм (heartbeat живости / sub-state `finalizing` / per-stage grace / ownership-lease на
> edge-гейты), точные имена символов/колонок/флагов и порядок врезок — **архитектурное решение**
> (06-adr), не зона аналитика. Модули-плейсхолдеры ниже выровнены под манифест `PIPELINE_DOCS.md`.
## 1. Сводка изменения
Сделать повторную обработку reaper'ом завершившегося-но-ещё-`running` job на стадии `deploy-staging`
**идемпотентной и владеющей состоянием**: пока исходный monitor/finalizer **жив** (или edge-гейты для
пары `(job, stage)` уже исполняются), reaper **не должен** независимо запускать второй прогон edge-
под-гейтов ребра `deploy-staging → deploy` (security / merge-gate / локальный re-test / coverage /
image-freshness) и **не должен** на этом основании откатывать задачу. Корень — ошибочное допущение
Tier-2 finalization-grace (`reaper_finalize_grace_s=300`), что финализация после штампа `finished_at`
длится «секунды…десятки секунд»; для `deploy-staging` она длится **минуты** (полный re-test + coverage
+ rebuild), потому что `_try_advance_stage` (тяжёлые edge-гейты) выполняется **после** штампа
`finished_at` и **до** `_finalize_job`. Фикс должен дать reaper'у способ отличить «живой, долго
финализирующий монитор» от «мёртвого монитора» и обеспечить строгое владение исполнением edge-гейтов.
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `src/job_reaper.py` | изменить — Tier-2-ветка `_reap_job` (строки ~197-209), `_reap_known_outcome` / `_reap_exit0` / `_gate_driven_advance`: ввести проверку живости/владения перед side-effectful re-drive `advance_stage` для `deploy-staging`; при живом finalizer'е**defer**, не reap |
| `src/agents/launcher.py` | изменить (вероятно) — `_monitor_agent`: место/порядок штампа `finished_at` (строка ~861) относительно `_try_advance_stage` (строка ~998) и/или эмиссия durable-сигнала «finalizer жив/финализирует» перед запуском тяжёлых edge-гейтов |
| `src/db.py` | изменить (вероятно) — `get_running_jobs` (строки ~1337-1367, источник `finished_age_s`) и/или аддитивная колонка владения/живости (`_ensure_column`, паттерн `tasks.cancelled_at`); `reap_running_job` — без изменения контракта атомарного флипа |
| `src/config.py` | изменить (вероятно) — kill-switch фикса и/или per-stage/finalize-aware grace; сохранить сквозной инвариант `reaper_max_running_s > Σ gate-work + grace` (NFR-6) |
| `src/stage_engine.py` | **только чтение/ссылка**`advance_stage` ребро `deploy-staging` (строки ~321-383): эталон того, какие edge-гейты дублируются. Изменение нежелательно; идемпотентность предпочтительно решать на стороне reaper/launcher |
> Точный набор затронутых модулей и распределение логики (reaper-only vs launcher-сигнал vs db-колонка)
> архитектор фиксирует в 06-adr. Аналитик фиксирует, что центр тяжести правки — `src/job_reaper.py`.
## 3. Функциональные требования
### FR-1 — Распознавание живости финализирующего монитора (BR-1, BR-3)
Reaper в Tier-2 для стадии `deploy-staging` должен распознавать ситуацию «процесс агента завершён,
но monitor/finalizer ещё **жив** и исполняет edge-гейты» и **не** трактовать её как мёртвый монитор по
одному лишь `finished_age_s >= reaper_finalize_grace_s`. Сигнал живости должен переживать долгую
(минуты) финализацию `deploy-staging`. **Инвариант:** живой finalizer **никогда** не reap'ается.
### FR-2 — Идемпотентность и строгое владение edge-гейтами (BR-2)
Для пары `(job, stage)` на `deploy-staging` тяжёлый прогон edge-гейтов (security / merge-gate /
локальный re-test / coverage / image-freshness) исполняется **не более одного раза одновременно**:
актор, не владеющий состоянием, **не** запускает второй `advance_stage`/re-test/merge-gate. Текущий
атомарный claim-before-act (`reap_running_job ... WHERE status='running'`) защищает только флип строки
job — требование расширяет защиту на **side-effectful исполнение edge-гейтов**.
### FR-3 — Согласование grace/бюджета (BR-3, NFR-6)
Длительность finalization-grace (или заменяющий её сигнал живости) должна покрывать фактический
wall-clock финализации `deploy-staging` = Σ(security + merge re-test `merge_retest_timeout_s=900` +
coverage + image rebuild). Любая правка grace/таймаутов сохраняет сквозной инвариант ORCH-065/109/110:
`reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace`.
### FR-4 — Сохранение добивания мёртвого finalizer'а (BR-4)
Реально **мёртвый** monitor/finalizer на `deploy-staging` (краш посреди финализации, сигнал живости
отсутствует/протух) по-прежнему добивается reaper'ом за ограниченное время по существующему контракту
(retry в пределах бюджета, иначе `failed` + Telegram; Tier-3 backstop как крайний предохранитель).
Reaper не становится no-op для `deploy-staging`.
### FR-5 — Отсутствие расхождения состояния (BR-5)
Исключить одновременный ложный откат `deploy-staging → development` и успешное завершение
`deploy → done` для одной задачи. После согласования у задачи — единственное консистентное
терминальное/стадийное состояние; ложный developer-retry не инкрементируется. Под-гейт merge-verify
(`deploy → done`, ORCH-071) остаётся единственным choke-point'ом в `done` — он **не** ослабляется.
## 4. Изменения API
Новых публичных эндпоинтов **не требуется**. Допустима **аддитивная read-only** наблюдаемость в
`GET /queue` (например, отметка «deploy-staging finalize in-progress / deferred-by-liveness» в блоке
`reaper`) — без изменения существующих ключей ответа.
## 5. Изменения схемы БД
Возможна **аддитивная** колонка владения/живости finalizer'а (например, durable-таймштамп
«finalizing heartbeat» или owner-токен на job/agent_run), вводимая идемпотентно через `_ensure_column`
(паттерн `tasks.cancelled_at` / `tasks.track`). Существующие таблицы/колонки/индексы и их семантика —
**без изменений**. Точная необходимость и форма — за архитектором (06-adr / 08-data-requirements).
## 6. Требования к новым/изменённым QG checks
**Нет.** `QG_CHECKS` и любой `check_*` (включая `check_staging_status`, `check_branch_mergeable`,
`check_coverage_gate`, `check_security_gate`, `check_staging_image_fresh`, `check_deploy_status`) — не
трогаются ни по составу, ни по семантике, ни по machine-verdict ключам. Багфикс — свойство страховочного
демона/финализатора, **не** Quality Gate.
## 7. Совместимость / регресс
- **Kill-switch:** фикс под выделенным флагом; `False` → строго прежнее поведение reaper (нулевая
регрессия).
- **Область:** поведение для **не-`deploy-staging`** стадий и путь добивания мёртвого монитора —
сохранены; self-hosting-only/скоуп репо согласовать с существующими leaf-паттернами (если вводится
per-repo разрез).
- **Обратимость:** раскат обратим выключением флага (+ откатом значений grace/таймаутов к дефолтам).
- **Never-raise / restart-safe / self-hosting (NFR-1/3/5):** любой сбой нового пути живости/владения
деградирует безопасно (reaper-тик продолжается); новый durable-маркер восстановим после рестарта;
фикс не рестартит прод и не пушит `main`.
- **Сквозной инвариант (NFR-6):** `reaper_max_running_s > Σ gate-work + grace` сохранён.
- **Анти-регресс:** структурный тест-гард (см. `04-test-plan.yaml`), полный `tests/` остаётся зелёным.

View File

@@ -0,0 +1,98 @@
---
work_item: ORCH-113
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-113 — BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что считается
провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам репозитория и тестам.
---
## AC-1 — Нет второго прогона edge-гейтов при живом finalizer'е
**Условие:** job на `deploy-staging` с `exit_code=0`, `finished_age_s >= reaper_finalize_grace_s`, но
исходный monitor/finalizer ещё **жив** (сигнал живости присутствует).
- **PASS:** reaper **не** вызывает `_gate_driven_advance`/`advance_stage` для этого job; второй прогон
security / merge-gate / локального re-test / coverage / image-freshness **не** запускается; reaper
логирует defer.
- **FAIL:** reaper запускает повторный `advance_stage` / любой edge-под-гейт, пока finalizer жив.
---
## AC-2 — Идемпотентность и строгое владение состоянием
**Условие:** монитор финализирует `deploy-staging` и параллельно срабатывает reaper-тик для того же
job/stage.
- **PASS:** тяжёлый прогон edge-гейтов (merge-gate / локальный re-test) исполняется **ровно один раз**
для пары `(job, stage)`; актор без владения состоянием не выполняет побочных шагов (мерж/re-test/
rollback).
- **FAIL:** второй локальный re-test/merge-gate запускается для того же job/stage без владения
состоянием (двойное исполнение edge-гейтов).
---
## AC-3 — Мёртвый finalizer по-прежнему добивается
**Условие:** monitor/finalizer на `deploy-staging` реально **умер** посреди финализации (сигнал
живости отсутствует/протух), job завис в `running`.
- **PASS:** reaper за ограниченное время добивает job по существующему контракту (retry в пределах
бюджета, иначе `failed` + Telegram; Tier-3 backstop как предохранитель); reaper для `deploy-staging`
**не** превращён в no-op.
- **FAIL:** мёртвый finalizer на `deploy-staging` не добивается reaper'ом (зомби-job блокирует очередь).
---
## AC-4 — Нет ложного отката и расхождения состояния после успешного deploy
**Условие:** сценарий инцидента ORCH-111 — долгая (> grace) финализация `deploy-staging` при зелёном
`staging_status: SUCCESS`, deploy/finalizer параллельно доходит до `deploy_status: SUCCESS` / merge PR.
- **PASS:** задача **не** откатывается `deploy-staging → development` параллельной reaper-веткой;
developer-retry **не** инкрементируется ложно; у задачи единственное консистентное
терминальное/стадийное состояние (сходимость, не расхождение).
- **FAIL:** задача откатана `deploy-staging → development` и/или начислен ложный developer-retry, в то
время как deploy фактически успешен; ветки состояния расходятся.
---
## AC-5 — Инварианты и контракт reaper сохранены
**Условие:** аудит диффа и поведения при выключенном kill-switch.
- **PASS:** `STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict ключи / схема
существующих таблиц — **байт-в-байт**; БД-правки только аддитивные (`_ensure_column` /
`CREATE TABLE IF NOT EXISTS`); reaper остаётся never-raise per unit; выключенный флаг → прежнее
поведение; правки не рестартят прод и не пушат `main`; сквозной инвариант
`reaper_max_running_s > Σ gate-work + grace` (ORCH-065/109/110) сохранён.
- **FAIL:** изменены `STAGE_TRANSITIONS`/`QG_CHECKS`/семантика `check_*`/machine-verdict ключи или
схема существующих таблиц; reaper может бросить исключение из тика; флаг `False` меняет поведение;
нарушен сквозной бюджетный инвариант.
---
## AC-6 — Обязательный регресс-тест и зелёный полный прогон
**Условие:** запуск тест-сюита репозитория.
- **PASS:** добавлен регресс-тест инцидента ORCH-111 (TC-05 в `04-test-plan.yaml`), **красный** на
коде до фикса и **зелёный** после; полный `pytest tests/ -q` зелёный.
- **FAIL:** регресс-теста нет, либо он зелёный и до фикса (не воспроизводит баг), либо полный регресс
`tests/` красный.
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / BR-3 / FR-1 |
| AC-2 | BR-2 / FR-2 |
| AC-3 | BR-4 / FR-4 |
| AC-4 | BR-5 / FR-5 |
| AC-5 | NFR-1 / NFR-2 / NFR-3 / NFR-6 / FR-3 |
| AC-6 | BR-1…BR-5 (регресс-доказательство) |

View File

@@ -0,0 +1,76 @@
work_item: ORCH-113
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
title: "job-reaper не повторяет финализацию deploy-staging при живом finalizer'е: живость + идемпотентность + строгое владение"
framework: pytest
scope: >
Покрывает: распознавание живого финализирующего монитора в Tier-2 reaper на стадии deploy-staging
(не reap по одному finished_age_s); идемпотентность и строгое владение исполнением edge-гейтов
(не более одного локального re-test/merge-gate на (job, stage)); сохранение добивания РЕАЛЬНО
мёртвого finalizer'а; отсутствие ложного отката deploy-staging -> development и расхождения состояния
после успешного deploy; сохранение инвариантов (STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/
схема существующих таблиц байт-в-байт; never-raise; kill-switch; сквозной бюджет ORCH-065/109/110).
Вне покрытия: инфра-толерантность merge-gate к таймауту re-test и tree-kill осиротевших процессов
(ORCH-110); починка конкретных мигающих тестов; поведение enduro/не-self репо (только проверяется
отсутствие регрессии / no-op).
notes: >
TC-05 — ОБЯЗАТЕЛЬНЫЙ регресс-тест инцидента ORCH-111 (deployer job 1914 / run_id 683): КРАСНЫЙ на
коде до фикса (reaper при живом долгом finalizer'е deploy-staging независимо запускает второй прогон
edge-гейтов и откатывает задачу), ЗЕЛЁНЫЙ после фикса. Подпроцессы (pytest re-test / coverage /
docker), сеть, Plane и Gitea — мокаются; «живой/мёртвый finalizer» и «долгая финализация > grace»
моделируются управляемо, без обращения наружу. Полный регресс tests/ должен оставаться зелёным.
Точные имена символов/колонок/флагов уточняет архитектор (06-adr); модули-плейсхолдеры выровнены под
манифест PIPELINE_DOCS.
tests:
- id: TC-01
type: unit
description: "Tier-2 reaper на deploy-staging: exit_code=0 и finished_age_s >= grace, но finalizer ЖИВ (сигнал живости присутствует) -> reaper НЕ вызывает _gate_driven_advance/advance_stage; второй прогон edge-гейтов не запускается; логируется defer (AC-1/FR-1)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-02
type: unit
description: "Строгое владение: при попытке повторной обработки того же (job, stage) актор без владения состоянием НЕ исполняет merge-gate/локальный re-test/advance (claim/ownership проигран -> ноль побочных эффектов), AC-2/FR-2."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-03
type: unit
description: "Мёртвый finalizer на deploy-staging (сигнал живости отсутствует/протух) -> reaper по-прежнему добивает job за ограниченное время по существующему контракту (retry в пределах бюджета, иначе failed+Telegram; Tier-3 backstop срабатывает) — reaper не no-op для deploy-staging (AC-3/FR-4)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-04
type: integration
description: "Идемпотентность под гонкой: монитор финализирует deploy-staging и параллельно срабатывает reaper-тик -> тяжёлый прогон edge-гейтов (merge-gate/re-test) исполняется РОВНО ОДИН раз для (job, stage); нет второго re-test и нет ложного rollback (AC-2/AC-4/FR-2/FR-5)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-05
type: integration
description: "ОБЯЗАТЕЛЬНЫЙ регресс ORCH-111: долгая (> grace) финализация deploy-staging при staging_status=SUCCESS, deploy/finalizer параллельно доходит до успеха/merge PR -> reaper НЕ откатывает deploy-staging -> development и НЕ инкрементирует developer-retry; у задачи единственное консистентное состояние. КРАСНЫЙ до фикса, ЗЕЛЁНЫЙ после (AC-4/FR-5)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-06
type: unit
description: "Регресс-гард совместимости: kill-switch выключен ИЛИ стадия не deploy-staging -> поведение reaper байт-в-байт прежнее (Tier-2 grace, claim-before-act, dead-pid/Tier-3 пути неизменны), NFR-4/AC-5."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-07
type: unit
description: "Сквозной инвариант бюджета (ORCH-065/109/110): при дефолтном конфиге reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace; любая правка grace/таймаутов фикса инвариант не нарушает (NFR-6/AC-5)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS
- id: TC-08
type: unit
description: "never-raise: сбой/исключение в новом пути распознавания живости/владения деградирует безопасно — reaper-тик не падает, прочие job обрабатываются, прод не трогается, main не пушится (NFR-1/NFR-3/AC-5)."
module: tests/test_orch113_reaper_finalizer_liveness.py
expected: PASS

View File

@@ -0,0 +1,158 @@
---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# ADR-001: Reaper Tier-2 — in-memory ownership-маркер финализации `deploy-staging` (живой finalizer не реапится)
Work Item: **ORCH-113** — BUG: job-reaper повторно запускает финализацию `deploy-staging`, пока жив исходный finalizer
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0043-reaper-finalizer-liveness-ownership.md`** (решение кросс-каттинговое — уточняет контракт reaper ORCH-065/adr-0011).
## Статус
Proposed
## Контекст
Оркестратор self-hosting: один инстанс, общая БД/очередь, `max_concurrency=1`. Финальный статус job
(`done`/`queued`/`failed`/`cancelled`) пишется **только** в живом процессе
(`launcher._monitor_agent → _finalize_job`). Сверено по коду:
- `_monitor_agent` штампит `agent_runs.finished_at`/`exit_code` **ПЕРВЫМ** (`launcher.py:861`), затем
делает git commit/push (+PR), usage-комментарии Plane (секунды…десятки секунд), затем
`_try_advance_stage` (`launcher.py:998`) и лишь потом `_finalize_job` (`launcher.py:1003`).
- На ребре `deploy-staging → deploy` `_try_advance_stage → advance_stage` синхронно, **в потоке
монитора**, прогоняет тяжёлый набор edge-под-гейтов (`stage_engine.py:327368`):
`security``merge-gate` (полный локальный re-test, бюджет `merge_retest_timeout_s=900`) →
`coverage` (`pytest --cov`) → `image-freshness` (docker-rebuild + пересоздание staging) — это
**минуты**, и весь объём идёт **после** штампа `finished_at` и **до** `_finalize_job`.
- Reaper Tier-2 (`job_reaper._reap_job`, `job_reaper.py:197209`) меряет `finished_age_s` от
`agent_runs.finished_at` (`db.get_running_jobs`, `db.py:1360`) = **от начала** финализации. По
истечении `reaper_finalize_grace_s=300` он трактует живого, долго финализирующего монитора как
мёртвого и независимо запускает тот же тяжёлый advance (`_reap_exit0 → _gate_driven_advance →
_try_advance_stage → advance_stage`).
Дешёвая read-only пред-проверка `_gate_is_green('deploy-staging')` читает лишь `check_staging_status`
(frontmatter `15-staging-log.md` = `SUCCESS`) → reaper уверенно идёт в тяжёлый advance. Атомарный
claim-before-act (`reap_running_job ... WHERE status='running'`) защищает **флип строки** job, но **не
side-effectful исполнение edge-гейтов**: монитор не claim'ит строку перед `advance_stage`, поэтому
монитор и reaper выполняют `advance_stage` **параллельно**.
**Инцидент ORCH-111 (deployer job 1914, run_id 683):** финализация `deploy-staging` заняла >300с;
reaper повторил edge-гейты; один повторный re-test стал красным (`3 failed … 14 errors in 444.79s`);
задача ложно откатана `deploy-staging → development` (+ ложный developer-retry), **параллельно**
исходный finalizer довёл deploy до `SUCCESS` и смержил PR #130 (`deploy → done`). Состояние раздвоилось.
Источника истины «жив ли finalizer» сегодня нет: pid агента в Tier-2 уже мёртв в **обоих** случаях
(`proc.wait()` вернулся), а живость **потока-монитора** система не наблюдает. Per-stage grace,
покрывающая Σ финализации (`Σ ≈ 4160с`), невозможна без нарушения сквозного бюджета ORCH-065/109/110
`reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace (≈4460)`.
**Решающий факт (проверен):** монитор и reaper — daemon-**потоки одного** uvicorn-процесса (CMD без
`--workers`; `_monitor_agent` стартует `threading.Thread`, `launcher.py:661`; reaper — daemon-поток,
`main.py:144`), разделяющие одну SQLite-БД. Значит, живость finalizer'а можно определить **in-memory**.
Рестарт покрыт существующим `requeue_running_jobs()` (`running → queued`), который в `main.lifespan`
вызывается (`main.py:59`) **до** старта reaper (`main.py:144`).
## Решение
### Сводка
Ввести **процесс-локальный реестр владения финализацией**: живой монитор регистрирует «я финализирую
job X», а reaper в Tier-2 на стадии `deploy-staging` **не реапит** job, чьё владение активно, и
переходит к Tier-3 backstop. Реестр in-memory — авторитетен в рамках одного процесса/БД; рестарт
покрыт `requeue_running_jobs`. Grace и `reaper_max_running_s` не меняются → сквозной бюджет цел. Под
глобальным kill-switch; **нулевое** изменение схемы БД и контрактов.
### D1 — Leaf `src/finalizer_liveness.py` (владение, FR-2)
Новый чистый процесс-локальный модуль (паттерн `serial_gate`/`coverage_gate`: never-raise, без сети/БД):
- `mark(job_id, run_id, stage)` — зарегистрировать активную финализацию;
- `clear(job_id)` — снять;
- `is_active(job_id) -> bool` — есть ли живое владение;
- `snapshot() -> dict` — read-only для наблюдаемости.
Состояние — `{job_id: {"run_id", "stage", "started_ts"}}` + `threading.Lock`. Собственного TTL нет —
ограничение по времени даёт Tier-3 (см. D3). Все функции изолированы `try/except` → дефолт
(`is_active` при ошибке → `False`, консервативно: не блокировать добивание).
### D2 — Эмиссия владения в `launcher._monitor_agent` (FR-1)
`mark(job_id, run_id, stage)` вызывается **сразу после** штампа `exit_code` (`launcher.py:864`, самый
ранний момент, когда reaper переходит в Tier-2; до этого pid агента жив → Tier-1 защищает). Хвост
финализации (git push … `_try_advance_stage``_finalize_job`) оборачивается в `try/finally`, в
`finally``clear(job_id)`. Так исключение **в потоке монитора** гарантированно снимает владение →
reaper добивает (FR-4). Только при `job_id is not None` (legacy `launch()` с `job_id=None` не в
`get_running_jobs`). Гибель **всего процесса** → рестарт → `requeue_running_jobs` → реестр пуст
(restart-safe без durable, NFR-5).
### D3 — Консультация reaper, scoped + Tier-3 backstop (FR-3, FR-4)
В `job_reaper._reap_job`, Tier-2-ветка (`exit_code` записан, `finished_age >= grace`): **перед**
`_reap_known_outcome` — если `settings.reaper_finalizer_liveness_enabled` **И** стадия задачи
(`_task_meta`) `== "deploy-staging"` **И** `finalizer_liveness.is_active(job_id)`**defer** (лог +
счётчик), **не** реапить через Tier-2, провалиться к Tier-3. Иначе — прежний путь
(`_reap_known_outcome; return`), байт-в-байт. **Tier-3** (`age >= reaper_max_running_s`) маркер
**игнорирует** — добивает всегда (ограниченное время; бюджет `5400 > Σ+grace ≈ 4460` гарантирует, что
легитимная финализация завершится до 5400 → ложного Tier-3-реапа живого finalizer'а нет).
### D4 — Скоуп и kill-switch (NFR-4)
Только глобальный `reaper_finalizer_liveness_enabled` (`config.py`, env
`ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`, дефолт `True`). **Без** per-repo разреза: баг общий для
любого репо со стадией `deploy-staging` (enduro тоже); per-repo оставил бы баг активным для части
репо. Это сознательный отход от leaf-паттерна `*_repos` (он для **гейтов, действующих на репо**; здесь
— наблюдатель-безопасность глобального демона). `False` → reaper никогда не консультирует маркер →
поведение байт-в-байт прежнее; стадии `!= deploy-staging` не консультируются → не тронуты.
### D5 — Наблюдаемость (TZ §4)
Счётчик `finalizer_defers_total` + размер `finalizer_liveness.snapshot()` в блоке `reaper`
`GET /queue`. Существующие ключи ответа не меняются; новых эндпоинтов нет.
### Инварианты
`STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict ключи / схема существующих
таблиц — **байт-в-байт**; **нулевое** изменение схемы БД; reaper остаётся never-raise per-unit;
`reaper_finalize_grace_s` и `reaper_max_running_s` **не меняются** (NFR-6 цел); фикс не рестартит прод
и не пушит `main` (NFR-3). Merge-verify (`deploy → done`, ORCH-071) — единственный choke-point в
`done`, не ослабляется (FR-5).
## Альтернативы
- **Per-stage grace, покрывающая Σ** — отвергнуто: нарушает бюджет `5400 > Σ+grace` (Σ≈4160 ⇒ grace
пришлось бы <1240, не покрывает Σ); таймер — это и есть источник бага.
- **Durable-колонка (finalizing-heartbeat / owner-токен)** — отвергнуто: один процесс/одна БД →
in-memory авторитетно; рестарт покрыт requeue; блокирующий re-test (900с) не может бить периодический
heartbeat из того же потока; durable добавляет миграцию и запись ради нулевой выгоды.
- **Sub-state `finalizing` в `jobs.status`** — отвергнуто: меняет семантику статуса, который читают
`claim_next_job`/`requeue_running_jobs`/`reconciler`/`reaper` (`WHERE status='running'`) — нарушение
NFR-2 и высокий радиус поражения.
- **Lease-файл на `(job, stage)` (как merge-lease)** — отвергнуто: тяжелее (файловый I/O, TTL,
reclaim), дублирует merge-lease; in-memory достаточно при одном процессе; TTL возвращает таймер-проблему.
- **Флип job из `running` до тяжёлых гейтов** — отвергнуто: ломает `get_running_jobs`/метрики и
restart-requeue (краш мид-гейт оставит job non-running и нереквью'имым).
## Последствия
- **+** Устранены повторный прогон edge-гейтов, ложный откат `deploy-staging → development` и
расхождение состояния при живом долгом finalizer'е; идемпотентность edge-гейтов через владение
(AC-1/AC-2/AC-4).
- **+** Реально мёртвый/застрявший finalizer добивается (finally-clear → Tier-2; иначе Tier-3);
основная функция reaper ORCH-065 сохранена (AC-3).
- **+** Нулевое изменение схемы и контрактов; сквозной бюджет ORCH-065/109/110 не тронут (AC-5).
- **** Гарантия владения валидна при **одном процессе/одной БД** (проверено: один uvicorn-воркер);
ввод `--workers>1` потребует durable-сигнала — зафиксировано в `10-tech-risks.md` (TR-3).
- **** Окно «штамп `finished_at``mark()`» (git push) маркером не покрыто — закрыто прежним
grace=300 (окно ≪ grace), TR-2.
- **Откат:** `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED=false` → reaper байт-в-байт прежний (маркер
по-прежнему пишется монитором, но не консультируется — инертен). Полный откат — удаление leaf +
двух врезок.
## Ссылки
- BRD: `docs/work-items/ORCH-113/01-brd.md`
- TRZ: `docs/work-items/ORCH-113/02-trz.md`
- Acceptance: `docs/work-items/ORCH-113/03-acceptance-criteria.md`
- Test-plan: `docs/work-items/ORCH-113/04-test-plan.yaml`
- Сквозной ADR: `docs/architecture/adr/adr-0043-reaper-finalizer-liveness-ownership.md`
- Базовые ADR: `adr-0011` (reaper/lease ORCH-065), `adr-0040` (timeout-бюджеты ORCH-109),
`adr-0042` (merge-gate re-test ORCH-110)
- Сверено по коду: `src/job_reaper.py`, `src/agents/launcher.py` (`_monitor_agent`/`_try_advance_stage`),
`src/db.py` (`get_running_jobs`/`requeue_running_jobs`), `src/stage_engine.py` (`advance_stage` ребро
`deploy-staging`), `src/config.py` (`reaper_*`), `src/main.py` (`lifespan`)
</content>

View File

@@ -0,0 +1,40 @@
---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 07 — Инфра-требования: ORCH-113 — reaper finalizer-liveness ownership
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: architecture
> When-applicable / информационный. Топология **не меняется**; ниже — только конфиг и операционные
> инварианты, которые сопровождающий обязан удержать.
## Изменения топологии
**N/A.** Ни новых сервисов/контейнеров, ни портов, ни томов, ни сетевых правил. Решение целиком внутри
процесса `orchestrator` (новый leaf + две врезки в существующие потоки monitor/reaper).
## Новый конфиг (env)
| Ключ | Дефолт | Назначение |
|------|--------|-----------|
| `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED` | `true` | Kill-switch. `false` → reaper байт-в-байт прежний (маркер пишется, но не консультируется). Откат фикса = установить `false`. |
Существующие `reaper_finalize_grace_s` (300) и `reaper_max_running_s` (5400) — **не меняются**.
`.env.example` пополнить новым ключом (дефолт = боевое значение, паттерн ORCH-101: пустой `.env`
прежнее поведение).
## Операционные инварианты (сопровождение)
- **Одно-процессная модель — несущий инвариант.** Авторитетность in-memory реестра владения держится
на том, что монитор и reaper — потоки **одного** uvicorn-процесса. CMD/команда compose **не должны**
получать `uvicorn --workers>1` без перевода сигнала в durable (см. `10-tech-risks.md` TR-3, ADR-001).
Сверено: `Dockerfile:65`, `docker-compose.yml:36` (prod), `docker-compose.yml:123` (staging) — без
`--workers`.
- **Сквозной бюджет ORCH-065/109/110** `reaper_max_running_s (5400) > Σ(deploy-staging gate-work)+grace
(≈4460)` остаётся в силе и фиксом не затрагивается (TR-4).
- **Self-hosting-страховка:** обкатка — на staging (8501, изолированная БД) до прод-деплоя; деплой
орка — только через статус «Confirm Deploy». Фикс не рестартит прод и не пушит `main`.
</content>

View File

@@ -0,0 +1,43 @@
---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 08 — Требования к данным: ORCH-113 — reaper finalizer-liveness ownership
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: architecture
> When-applicable / информационный (гейтом не парсится).
## Изменения схемы БД
**N/A — нулевое изменение схемы.** Сознательное архитектурное решение (ADR-001 / adr-0043): сигнал
владения финализацией — **in-memory** (leaf `src/finalizer_liveness.py`), а не durable-колонка. Ни
новых таблиц, ни новых колонок, ни индексов; `init_db()` / `_ensure_column` не трогаются. Схема
существующих таблиц (`jobs`, `agent_runs`, `tasks`, …) и их семантика — **байт-в-байт** (NFR-2/AC-5).
## Новые/изменённые сущности
**Процесс-локальный реестр владения** (не БД): `finalizer_liveness` хранит
`{job_id: {"run_id", "stage", "started_ts"}}` под `threading.Lock`. Запись/снятие — живой
монитор-поток (`launcher._monitor_agent`); чтение — reaper-поток (`job_reaper`). Ключ — `jobs.id`
(существующая сущность). Никаких новых персистентных данных.
## Совместимость данных / миграции
- **Миграций нет** — нечего мигрировать (нет схемных изменений); общая прод-БД (self-hosting +
enduro-trails) не затрагивается.
- **Restart-safe без durable (NFR-5):** in-memory реестр сбрасывается при рестарте процесса, что
**безопасно** по существующему контракту: `main.lifespan` вызывает `requeue_running_jobs()`
(`running → queued`, `main.py:59`) **до** старта reaper (`main.py:144`). После рестарта нет ни одного
`running`-job, ссылающегося на потерянный маркер → отсутствие маркера корректно (нет живых
finalizer'ов). Гибель **потока** монитора (не процесса) покрыта `try/finally`-снятием маркера; гибель
**процесса** → рестарт → requeue.
- **Авторитетность in-memory** опирается на одно-процессную модель (один uvicorn-воркер, общая
SQLite-БД; проверено: CMD без `--workers`). Условие задокументировано как инвариант сопровождения —
при вводе `--workers>1` сигнал должен стать durable (см. `10-tech-risks.md` TR-3).
</content>

View File

@@ -0,0 +1,37 @@
---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-113 — reaper finalizer-liveness ownership
Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Риски реализации и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Над-толерантность:** маркер «жив» застрял (не снят) → реально мёртвый finalizer не добивается, зомби клинит очередь (регресс ORCH-065). | Низ. | Выс. | `try/finally`-снятие в `_monitor_agent` (исключение потока снимает владение); гибель процесса → рестарт → `requeue_running_jobs`. **Tier-3 backstop игнорирует маркер** и добивает при `age >= reaper_max_running_s=5400` → ограниченное время гарантировано (FR-4/AC-3). Покрытие — TC-03. |
| TR-2 | **Окно без владения** между штампом `finished_at` (launcher:861) и `mark()` (после exit_code, launcher:864): reaper мог бы реапнуть в этом окне. | Низ. | Сред. | Окно = git push/PR/Plane-комментарии (секунды…десятки секунд) ≪ `reaper_finalize_grace_s=300` → прежний grace покрывает его; маркер ставится самым ранним возможным моментом Tier-2 (до этого pid агента жив → Tier-1 защищает). |
| TR-3 | **Многопроцессность:** при `uvicorn --workers>1` монитор и reaper окажутся в разных процессах → in-memory реестр не разделяется → возможна двойная финализация. | Низ. | Выс. | Сейчас CMD без `--workers` (проверено: `Dockerfile:65`, `docker-compose.yml:36`). Инвариант сопровождения зафиксирован в ADR-001/adr-0043 и 08-data-requirements: ввод `--workers>1` ⇒ перевести сигнал в durable (heartbeat-колонка) — отдельная задача. Анти-дрейф можно усилить структурным тестом (нет `--workers` в CMD). |
| TR-4 | **Нарушение сквозного бюджета** ORCH-065/109/110 при правке grace/таймаутов. | Оч. низ. | Выс. | Решение **не меняет** `reaper_finalize_grace_s` (300) и `reaper_max_running_s` (5400) — инвариант `5400 > Σ(deploy-staging gate-work)+grace ≈ 4460` тривиально цел; покрытие — TC-07/AC-5. |
| TR-5 | **Гонка чтения/записи** реестра (монитор пишет, reaper читает). | Низ. | Сред. | `threading.Lock` вокруг операций реестра; `is_active`/`snapshot` атомарны под локом; never-raise → ошибка чтения = `False` (консервативно, не блокирует добивание). Покрытие — TC-02/TC-04/TC-08. |
| TR-6 | **Регресс не-deploy-staging / выключенного флага** (NFR-4): фикс случайно меняет прежние пути reaper. | Низ. | Сред. | Консультация маркера gated `enabled AND stage=="deploy-staging"`; Tier-1/Tier-3/exit≠0/claim-before-act не трогаются; `False` → reaper байт-в-байт прежний. Покрытие — TC-06. |
| TR-7 | **Ложный regression-тест** TC-05: зелёный и до фикса (не воспроизводит баг). | Сред. | Сред. | TC-05 моделирует «живой долгий finalizer > grace» управляемо (моки подпроцессов/сети); обязан быть **красным до** фикса и **зелёным после** (AC-6). Reviewer/tester проверяют красноту на базе. |
## Сводный вывод
Доминирующий класс — **над-толерантность** (TR-1) и **многопроцессная авторитетность** (TR-3); оба
имеют низкую вероятность и закрыты соответственно Tier-3 backstop'ом (без правки бюджета) и
зафиксированным инвариантом одно-процессной модели. Решение аддитивно, под kill-switch, без изменения
схемы/контрактов и без правки сквозного бюджета. Эскалация `arch:major-change` **не требуется**
(нет новой стадии/QG, нет изменения схемы БД, центр тяжести — один leaf + две точечные врезки).
Остаточный риск для прод-конвейера (self-hosting) — **низкий**; полностью обратим выключением
`ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`.
</content>

View File

@@ -0,0 +1,86 @@
---
verdict: APPROVED
work_item: ORCH-113
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-15
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-113
version: 1
---
# Review ORCH-113 — BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer
## Summary
Фикс инцидента ORCH-111 реализован чисто и полно. Введён процесс-локальный реестр владения
финализацией (`src/finalizer_liveness.py`, never-raise leaf по паттерну `serial_gate`/`coverage_gate`):
монитор `mark()`-ит владение сразу после штампа `exit_code` и `clear()`-ит его в `finally` хвоста
финализации; reaper в Tier-2 при `stage=="deploy-staging"` И активном владении делает **defer** вместо
повторного `advance_stage`, проваливаясь к Tier-3 backstop (который маркер игнорирует → застрявший
finalizer всё равно добивается).
Проверено по всем 4 осям; блокирующих findings нет.
- **ТЗ:** FR-1…FR-5 реализованы; AC-1…AC-6 покрыты тестами `tests/test_orch113_reaper_finalizer_liveness.py`
(TC-01…TC-08). Схема БД — **нулевое** изменение (выбран in-memory реестр), что строже допущенной ТЗ §5
«аддитивная колонка». API §4 (read-only ключи в `GET /queue`) и QG §6 (не трогать) — соблюдены.
- **ADR:** реализация байт-в-байт соответствует ADR-001 / сквозному adr-0043 (D1D5). Трассировка
сохранена: авторитет Tier-3 (adr-0011/ORCH-065) и сквозной бюджет `reaper_max_running_s (5400) >
Σ(gate-work)+grace` (ORCH-109/110) не нарушены — зафиксировано регресс-тестом TC-07. Ни один
маркированный инвариант не сломан.
- **Качество кода:** хвост `_monitor_agent` вынесен в `_run_monitor_finalization` **дословно**
подтверждено `git diff -w` (+49/0, нулевое изменение логики); все переменные, на которые ссылается
извлечённое тело, — параметры/локальные/модульные (нет риска `NameError`, проверено вручную).
never-raise во всех публичных функциях и врезках. Обязательный регресс-тест багфикс-трека (ORCH-019
BR-4 / coverage ORCH-027) присутствует: TC-05 по построению КРАСНЫЙ до фикса (assert `calls == []`,
который pre-fix reaper нарушил бы вызовом `_try_advance_stage`) и ЗЕЛЁНЫЙ после.
- **Документация:** обновлены в том же PR — `docs/architecture/README.md` (описание Job-reaper +
раздел Tier-2 + список kill-switch + ссылки на ADR), `docs/architecture/internals.md` (детализация
Tier-2), `CHANGELOG.md`, ADR-001 (work-item) и сквозной adr-0043; все номерные доки задачи (0004,
06-adr, 07, 08, 10) на месте.
**Проверка прогона:** `pytest tests/ -q`**2001 passed**, 0 failures (AC-6); целевой файл — 13 passed.
## Findings
### P0 — Blocker
-ет_
### P1 — Must fix
-ет_
### P2 — Should fix
-ет_
### P3 — Nice-to-have (не блокирует приёмку)
- [ ] Frontmatter обоих ADR (`ADR-001` и `adr-0043`) держит `status: proposed`. По мере мержа фикса
статус естественно становится принятым решением — стоит при следующем касании обновить на `accepted`
(косметика трассировки, не влияет на гейт).
- [ ] В врезке `mark()` (`launcher._monitor_agent`, стр. ~884) делается отдельный
`get_task_by_repo_branch(repo, branch)` ради `stage`-контекста, хотя тот же lookup повторяется ниже в
хвосте финализации (стр. ~984). Дублирование на пути, и так делающем БД-работу, обёрнуто never-raise;
`stage` здесь — best-effort контекст для `snapshot()` (reaper резолвит стадию независимо через
`_task_meta`), так что корректность не зависит от него. Можно при желании переиспользовать один lookup.
## Документация
**Статус: полностью обновлена в том же PR (golden source соблюдён).**
| Артефакт | Изменение | Оценка |
|----------|-----------|--------|
| `docs/architecture/README.md` | Job-reaper компонент + раздел Tier-2 + список kill-switch (`ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`) + ссылки на adr-0043 | ✅ |
| `docs/architecture/internals.md` | Детализация Tier-2 deploy-staging defer | ✅ |
| `CHANGELOG.md` | Развёрнутая запись `[Unreleased]` с подпунктами (leaf / эмиссия / консультация / наблюдаемость) | ✅ |
| `docs/work-items/ORCH-113/06-adr/ADR-001-…` | Детальный ADR (D1D5, альтернативы, последствия) | ✅ |
| `docs/architecture/adr/adr-0043-…` | Сквозной ADR (уточняет adr-0011/0040/0042/0041) | ✅ |
| `docs/work-items/ORCH-113/{00..04,07,08,10}` | Полный пакет номерных доков | ✅ |
**Обзорные доки / витрина:** правка внутренняя для job-reaper; высокоуровневые описания в
`docs/overview/tech-architecture.md` («job-reaper возвращает в очередь job'ы, чей исполнитель умер») и
`README.md` остаются корректными — обновления не требуют (ORCH-079/ORCH-011 не задеты). Раздел README
«Известные ограничения» не содержит пункта, закрываемого этим PR (баг был инцидентом, не значился
ограничением) — обновление не требуется. Известное ограничение `--workers>1` (TR-3) — системное
пред-допущение, документировано в `10-tech-risks.md` и обоих ADR; вынос в README не обязателен.

View File

@@ -0,0 +1,66 @@
---
result: PASS # PASS | FAIL — машинный вердикт, UPPERCASE
work_item: ORCH-113
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-15
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-113
---
# Test Report — ORCH-113
BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer.
## Окружение
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-113-bug-job-reaper-must-not-re-run` (ветка `feature/ORCH-113-bug-job-reaper-must-not-re-run`)
- Python: 3.12.13
- pytest: 8.3.3 (plugins: cov-5.0.0, anyio-4.13.0, asyncio-0.23.8)
- Дата: 2026-06-15
- Review verdict (12-review.md): `APPROVED`
## Smoke API (read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — PASS |
| `GET /status` | Отвечает; ORCH-113 (task 101) виден на стадии `testing` — PASS |
| `GET /queue` | Блоки `serial_gate` (ORCH-088) **и** `auto_labels` (ORCH-089) присутствуют — PASS |
## Результаты — покрытие тест-плана (04-test-plan.yaml)
Все TC реализованы в `tests/test_orch113_reaper_finalizer_liveness.py` (13 тест-функций на 8 TC).
| TC ID | Описание | AC / FR | Тест-функция | Результат |
|-------|----------|---------|--------------|-----------|
| TC-01 | Живой finalizer на `deploy-staging` (exit=0, age≥grace) → reaper НЕ вызывает `_gate_driven_advance`/`advance_stage`, логирует defer | AC-1/FR-1 | `test_tc01_live_finalizer_deploy_staging_not_reaped` | PASS |
| TC-02 | Строгое владение: актор без владения НЕ исполняет merge-gate/re-test/advance (ноль побочных эффектов) | AC-2/FR-2 | `test_tc02_non_owner_runs_no_edge_gates` | PASS |
| TC-03 | Мёртвый finalizer → reaper по-прежнему добивает job (Tier-2 retry + Tier-3 backstop игнорирует маркер); reaper не no-op | AC-3/FR-4 | `test_tc03_dead_finalizer_still_reaped_tier2`, `test_tc03_tier3_backstop_ignores_marker` | PASS |
| TC-04 | Идемпотентность под гонкой: тяжёлый прогон edge-гейтов исполняется ровно один раз для `(job, stage)`, нет второго re-test/ложного rollback | AC-2/AC-4/FR-2/FR-5 | `test_tc04_idempotent_no_second_advance_under_race` | PASS |
| TC-05 | **ОБЯЗАТЕЛЬНЫЙ регресс ORCH-111:** долгая (>grace) финализация при `staging_status=SUCCESS` → нет отката `deploy-staging → development`, нет ложного developer-retry; единственное консистентное состояние (красный до фикса, зелёный после) | AC-4/FR-5 | `test_tc05_orch111_no_false_rollback_no_retry_increment` | PASS |
| TC-06 | Регресс-гард совместимости: kill-switch off ИЛИ не-`deploy-staging` → поведение reaper байт-в-байт прежнее | NFR-4/AC-5 | `test_tc06_killswitch_off_byte_for_byte_prior`, `test_tc06_non_deploy_staging_stage_not_consulted`, `test_tc06_within_grace_unchanged` | PASS |
| TC-07 | Сквозной инвариант бюджета: `reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace` (ORCH-065/109/110) | NFR-6/AC-5 | `test_tc07_budget_invariant_preserved` | PASS |
| TC-08 | never-raise: сбой пути живости/владения деградирует безопасно — reaper-тик не падает, прочие job обрабатываются | NFR-1/NFR-3/AC-5 | `test_tc08_liveness_error_never_breaks_tick`, `test_tc08_reap_once_isolates_and_never_raises`, `test_tc08_finalizer_liveness_leaf_never_raises` | PASS |
**Сопоставление с 03-acceptance-criteria.md:** AC-1…AC-6 покрыты (AC-1→TC-01, AC-2→TC-02/TC-04, AC-3→TC-03, AC-4→TC-04/TC-05, AC-5→TC-06/TC-07/TC-08, AC-6→полный зелёный прогон + TC-05 как регресс-доказательство). Каждый TC из тест-плана выполнен и сопоставлен.
## Вывод pytest
Целевой файл:
```
tests/test_orch113_reaper_finalizer_liveness.py ... 13 passed, 1 warning in 3.60s
```
Полный регресс:
```
$ python -m pytest tests/ -q
........................................................................ [ 39%]
... (snip) ...
......................................................... [100%]
2001 passed, 1 warning in 316.72s (0:05:16)
```
(единственный warning — `PydanticDeprecatedSince20` в `src/config.py:8`, не относится к задаче, присутствует исторически)
## Итог
**PASS** — целевой файл 13/13 PASS, полный регресс `tests/` 2001 passed / 0 failed, smoke API (`/health`, `/status`, `/queue` с блоками `serial_gate` + `auto_labels`) зелёный, каждый TC тест-плана выполнен и сопоставлен с критериями приёмки. Задача переходит на `deploy-staging`.

View File

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

View File

@@ -0,0 +1,42 @@
---
staging_status: SUCCESS
work_item: ORCH-113
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-15
model_used: claude-opus-4-8
timestamp: 2026-06-15T10:04:59Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live staging environment
(`orchestrator-staging`, port 8501, mode=stub), run inside the container via the
Docker Exec API (canonical `docker exec` path — host `docker` CLI unavailable;
ADR-001/ORCH-048 invariant preserved: B6 registry-isolation reads the running
instance's own `.env.staging` process-env).
## Result
```
RESULT: 8/10 checks PASS
REAL failed : none
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
tolerance: staging_infra_tolerance_enabled=True
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
## Verdict mapping (ORCH-061)
- Suite exit code: **0**`staging_status: SUCCESS`.
- All REAL checks green (Blocks A/B SMOKE+ACCESS, C7/C8 E2E).
- C9a/C9b are the two tolerated sandbox-infra checks (depend on SANDBOX bot
accounts being project members — not on the pipeline). They were waived per
ORCH-061; the script printed the `INFRA-WAIVED:`/`VERDICT:` lines and exited 0.
Exit code is trusted; waived checks are not re-judged.
Staging gate passed → advance to `deploy`.

View File

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

View File

@@ -0,0 +1,22 @@
---
coverage_status: PASS
work_item: ORCH-113
measured_coverage: 80.02
baseline: 79.95
floor: 0.00
policy: both
epsilon: 0.50
delta: 0.07
---
# Coverage Report — ORCH-113
Детерминированный гейт покрытия (ORCH-027) — под-гейт ребра `deploy-staging→deploy` (ПОСЛЕ merge-gate, ДО image-freshness). Машинный вердикт читается ТОЛЬКО из `coverage_status:` frontmatter выше.
## Verdict
measured=80.02% policy=both eps=0.50: absolute 80.02% >= floor 0.00%-eps0.50 -> PASS; baseline 80.02% >= base 79.95%-eps0.50 -> PASS
## Measurement
pytest --cov=src: line coverage src/ = 80.02%
## Policy
policy=both, floor=0.0%, baseline=79.95%, epsilon=0.5%

View File

@@ -0,0 +1,7 @@
# Business Request: BUG: pipeline stage transitions need ownership lease and smart startup recovery
Work Item ID: ORCH-114
## Description
TBD

View File

@@ -0,0 +1,186 @@
---
work_item: ORCH-114
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 01 — BRD (бизнес-требования): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis
> **Багфикс-трек → ЭСКАЛАЦИЯ В ПОЛНЫЙ ЦИКЛ (`escalate: full-cycle`).** Задача пришла под меткой
> `Bug` (укороченный маршрут ORCH-019, пропуск `architecture`), но дефект **системный и
> архитектурный**: вводится глобальный инвариант владения переходом, durable-механизм, переживающий
> рестарт процесса, и compare-and-swap на запись стадии. Это требует **ADR** (выбор механизма:
> lease / heartbeat / transition-epoch / CAS) и затрагивает поведение всего конвейера и нескольких
> фоновых акторов. Поэтому выпускается **полный** analysis-пакет; оператор снимает багфикс-трек
> эндпоинтом `POST /bug-fast-track/escalate?work_item=ORCH-114` → задача уходит в `architecture`
> (ADR-001 D5 ORCH-019).
---
## 1. Бизнес-контекст и проблема
ORCH-114 — **системный наследник** инцидент-цепочки ORCH-110 / ORCH-111 / ORCH-112 / ORCH-113.
Каждый предшественник закрыл **точечный** симптом, но **корневой класс** остался открыт:
**у side-effectful переходов стадий нет единого владения (ownership)**.
### Корень (установленный факт, верифицировано кодом)
`stage_engine.advance_stage(...)` — единая точка перехода между стадиями и исполнения тяжёлых
под-гейтов ребра `deploy-staging → deploy` (security → merge-gate re-test → coverage →
image-freshness) и под-гейта `deploy → done` (`_handle_merge_verify`: `merge_pr`, ratchet
coverage-baseline, запись proof-of-merge). При этом:
- **Запись стадии не атомарна по предусловию.** `db.update_task_stage(task_id, stage)`
«голый» `UPDATE tasks SET stage=? WHERE id=?` **без** `WHERE stage=?` (нет compare-and-swap, нет
epoch/version-колонки). Любой второй вызов безусловно перезатирает результат первого.
- **`advance_stage` ре-ентерабельна без защиты.** Внутри неё нет ни in-memory-лока на `task_id`,
ни durable-маркера «переход в процессе». Два конкурентных вызова для одной задачи оба читают
`stage='deploy-staging'`, оба прогоняют ВСЕ под-гейты, оба пишут `deploy`, оба ставят
следующего агента.
- **Минимум 5 путей входят в переход независимо:** (1) монитор агента (`launcher._try_advance_stage`,
auto-advance по `exit_code==0`), (2) Plane-webhook (`webhooks/plane._try_advance_stage`,
Approved / Confirm Deploy), (3) reconciler F-1 (`advance_if_gate_passed → advance_stage`,
`finished_agent=None`), (4) job-reaper (`job_reaper._gate_driven_advance → launcher._try_advance_stage`),
(5) deploy-finalizer Phase C (`run_deploy_finalizer → advance_stage(finished_agent="deployer")`).
Ни один не проверяет, не находится ли **другой** актор уже внутри того же перехода.
### Почему предшественники не закрыли класс
| Задача | Что закрыла | Что осталось открытым |
|--------|-------------|------------------------|
| **ORCH-110** | merge-gate re-test: ложный rollback по инфра-таймауту + tree-kill осиротевших pytest | Только merge-gate re-test; не вводит владения переходом |
| **ORCH-112** | гигиена общего deploy-checkout (грязь блокировала `git pull`) | Только чистка артефактов; не про конкурентные переходы |
| **ORCH-113** | reaper не пере-исполняет **живую** финализацию `deploy-staging` (Tier-2) | **process-local in-memory** реестр (`finalizer_liveness`), **только reaper**, **только Tier-2**, **только `deploy-staging`**; **теряется при рестарте**; **НЕ** покрывает reconciler / webhook / restart-recovery |
Таким образом, **остаточный кросс-путь** (ORCH-113 §ограничения сам это фиксирует): живой монитор
внутри `advance_stage(deploy-staging)` — и параллельно reaper (при выключенном liveness-флаге или
**после рестарта**, когда in-memory реестр пуст), либо reconciler F-1, либо webhook-путь — повторно
входят в тот же переход. Результат — **двойные** эффекты (security/merge/coverage/image-freshness/
прод-деплой) и **противоречивые** исходы (один путь откатывает на `development`, другой доводит до
`done`). Именно это наблюдалось в инциденте ORCH-111 (job 1914 / PR #130): повторный re-test покраснел
и дал ложный откат `deploy-staging → development` с ложным developer-retry, **одновременно** с успешной
финализацией и мержем оригинального монитора.
### Особый разрез — рестарт процесса (self-hosting)
Прод-контейнер `orchestrator` рестартится при self-деплое. Если процесс умирает **в середине**
финализации, in-memory `finalizer_liveness._OWNED` исчезает, `requeue_running_jobs` переводит
`running → queued`, и переход может быть **пере-исполнен с нуля** без знания, что часть необратимых
шагов (мерж в `main`, ratchet baseline, прод-деплой) уже применена. **Durable**-сигнал владения,
переживающий рестарт, отсутствует — это ключевая дельта ORCH-114 над ORCH-113.
---
## 2. Объём (scope)
### В объёме
- Единый **инвариант владения** side-effectful переходом/финализацией: в любой момент времени
переход конкретной задачи исполняет **не более одного** актора.
- **Compare-and-swap (CAS)** / transition-epoch на запись стадии: писатель применяет переход
только если предусловие (текущая стадия / эпоха) не изменилось с момента чтения; проигравший —
аборт **без** побочных эффектов.
- **Durable** механизм владения (lease/heartbeat/epoch — выбор за архитектором), переживающий
рестарт процесса.
- Осведомлённость **job-reaper** и **startup-requeue** о живой / устаревшей финализации (обобщение
ORCH-113 за пределы Tier-2 / `deploy-staging` / in-memory).
- **Reconciler F-1** и **webhook**-пути: skip/defer при активном lease перехода.
- **Умное восстановление при старте**: после смерти процесса в середине финализации система сходится
к **единственному** согласованному исходу (без двойных необратимых эффектов и без противоречий
rollback↔done).
- **Наблюдаемость**: read-only блок в `GET /queue` + алерт на форсированный/устаревший реклейм lease.
- **Регресс-тесты**: `deploy-staging`-ребро, deploy-finalizer (Phase C), restart-recovery.
### Вне объёма
- Изменение состава/порядка стадий (`STAGE_TRANSITIONS`), реестра `QG_CHECKS`, семантики/имён
`check_*`, машинных вердикт-ключей (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/
`security_status:`/`coverage_status:`) — **байт-в-байт не трогаются**.
- Повторная починка частных симптомов ORCH-110/112 (merge-retest tree-kill, checkout-hygiene) —
они уже закрыты; ORCH-114 их **переиспользует**, не переписывает.
- Переход на `uvicorn --workers>1` / мульти-процессную модель (остаётся одно-процессной; durable-lease
лишь делает инвариант корректным и на этот случай, но миграция модели — отдельная задача).
- Выбор конкретного механизма (lease vs heartbeat vs epoch), точная форма хранения (доп. таблица vs
доп. колонки) и порядок старта демонов — **решает архитектор** в `06-adr/` (это требования к
свойствам, не к реализации).
---
## 3. Заинтересованные стороны
- **Owner / оператор self-hosting** — заказчик; страдает от ложных откатов, двойных деплоев и ручного
разбора расхождений состояния.
- **Все проекты в общем инстансе** (orchestrator + enduro-trails) — групповой риск: расхождение
состояния и ложный freeze репо клинят общую очередь.
- **Принимает результат:** Owner; технически — финальная стадия конвейера (CI/гейты), не агент сам.
---
## 4. Бизнес-требования (BR)
| ID | Требование (проверяемое) | Покрытие |
|----|---------------------------|----------|
| **BR-1** | Side-effectful переход/финализацию задачи в любой момент исполняет **не более одного** актора (единое владение). | FR-1 / AC-1 |
| **BR-2** | Запись стадии для side-effectful переходов защищена **compare-and-swap / epoch**: проигравший гонку писатель не мутирует стадию и **не выполняет** побочных эффектов. | FR-2 / AC-1, AC-2 |
| **BR-3** | **job-reaper** осведомлён о живой vs устаревшей финализации на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом владении, реклейм мёртвого/устаревшего владельца в **ограниченное** время. | FR-3 / AC-4, AC-5 |
| **BR-4** | **Startup-requeue / восстановление при старте** учитывает незавершённую финализацию через durable-состояние: не пере-исполняет уже применённый необратимый шаг. | FR-4 / AC-6 |
| **BR-5** | **Reconciler F-1** и **webhook**-пути продвижения **пропускают/откладывают** переход, пока активен lease владения для задачи. | FR-5 / AC-7, AC-8 |
| **BR-6** | После смерти процесса в середине финализации система сходится к **единственному** согласованному исходу: **нет** двойного `merge_pr` / ratchet baseline / image-rebuild / инициации прод-деплоя и **нет** противоречия rollback↔done. | FR-1…FR-4 / AC-1, AC-6 |
| **BR-7** | Состояние владения переходом **наблюдаемо**: read-only блок в `GET /queue` + алерт при форсированном/устаревшем реклейме. | FR-6 / AC-12 |
| **BR-8** | Поставляются **регресс-тесты** на конкурентный двойной эффект (`deploy-staging`), deploy-finalizer (Phase C) и restart-recovery; обязательный регресс воспроизводит исходный класс (красный до фикса, зелёный после). | FR-7 / AC-1, AC-6, тест-план |
| **BR-9** | Механизм **обратим**: kill-switch возвращает поведение **байт-в-байт** к состоянию до ORCH-114. | FR-7 / AC-9 |
---
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| **NFR-1** | **never-raise.** Любая ошибка механизма владения изолируется. Горячий путь claim/guard — **fail-open** (не заклинить общую очередь всех проектов, AC-8 ORCH-088); решения, критичные для безопасности прода/необратимости — **fail-closed**. |
| **NFR-2** | **Kill-switch + область раската** по образцу leaf-гейтов (`serial_gate`/`coverage_gate`/`finalizer_liveness`): глобальный флаг + при необходимости CSV-скоуп репо (пусто → self-hosting only). При выключенном флаге — нулевая регрессия (enduro не затронут). |
| **NFR-3** | **Инварианты конвейера не тронуты:** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / машинные вердикт-ключи / схемы существующих таблиц — байт-в-байт. Любое новое хранилище — **аддитивно и идемпотентно** (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`). |
| **NFR-4** | **Durable / restart-safe.** Сигнал владения переживает рестарт процесса (ключевая дельта над in-memory `finalizer_liveness` ORCH-113); после рестарта восстановление детерминированно решает «дорешать vs уже применено». |
| **NFR-5** | **Self-hosting безопасность.** Механизм владения сам по себе **никогда** не рестартит прод-контейнер, не пушит/force-push в `main`, не трогает detached deploy-процесс (NFR-3 ORCH-090/112). |
| **NFR-6** | **Сквозной бюджет reaper сохранён:** инвариант ORCH-065/109/110/113 `reaper_max_running_s (5400) > Σ(deploy-staging gate-work ≈4460) + grace`. Lease **не** удлиняет финализацию за backstop без согласованной правки бюджета; устаревший/мёртвый владелец добивается Tier-3 в ограниченное время. |
| **NFR-7** | **Идемпотентность.** Повторный заход в уже применённый переход — **no-op** (по epoch / SHA-in-main / lease), никогда не второй побочный эффект. |
| **NFR-8** | **Обратная совместимость.** При флаге off / репо вне области — путь старта, claim и переходы байт-в-байт прежние (enduro и текущий orchestrator). |
---
## 6. Допущения и ограничения
- **Одно-процессная модель сейчас** (один uvicorn-воркер без `--workers`), но требование NFR-4
(durable) делает инвариант корректным и при будущем рестарте/мульти-процессности — без переписывания.
- **Источник истины планировщика — локальная БД** (offline hot-path, NFR-2/ORCH-026/088): механизм
владения не должен вносить сетевых зависимостей в горячий claim.
- **Переиспользуются существующие durable-примитивы:** атомарный `reap_running_job` (rowcount-guard),
`claim_next_job` (rowcount-guard), `requeue_running_jobs`, merge-lease (ORCH-043). ORCH-114 **достраивает**
владение поверх них, а не дублирует.
- **`finalizer_liveness` (ORCH-113)** — отправная точка: ORCH-114 обобщает её до durable, кросс-путевого
владения; решение «расширить / заменить / надстроить» принимает архитектор.
- Точные **D-решения** (durable shape, эпоха vs lease-таблица, набор покрываемых рёбер сверх
`deploy-staging`/`deploy→done`, порядок старта демонов) — за архитектором (`06-adr/`, `10-tech-risks.md`).
## 7. Критерии успеха
Кратко (детальные PASS/FAIL — `03-acceptance-criteria.md`):
- Конкурентный/после-рестартовый повторный вход в side-effectful переход **не** даёт двойных эффектов
и противоречивых исходов; ровно один актор владеет и доводит переход.
- CAS/epoch на запись стадии: проигравший — чистый аборт.
- reaper / startup / reconciler / webhook осведомлены о живом lease (defer) и о мёртвом (реклейм в
ограниченное время).
- Полный `pytest tests/` зелёный; новые регресс-тесты (двойной эффект, restart-recovery) зелёные;
при выключенном kill-switch — поведение байт-в-байт прежнее.
## 8. Риски
Краткий перечень (детали — `10-tech-risks.md`, заполняет архитектор):
- **Дедлок / over-block:** слишком «жёсткое» владение может заклинить легитимный путь (reaper не
добьёт зависший финализатор) → требование NFR-6 (bounded reclaim) и fail-open на hot-path.
- **Бюджет vs lease:** lease, удерживаемый дольше `reaper_max_running_s`, конфликтует со сквозным
бюджетом → согласование с ORCH-065/109/110/113.
- **Durable-состояние и гонки на рестарте:** некорректный «умный recovery» может сам стать источником
двойного применения → обязательный restart-recovery регресс (BR-8).
- **Скрытые пути перехода** (gitea-webhook `handle_push`/`handle_ci_status`/`handle_pr` пишут стадию
**в обход** `advance_stage` через прямой `update_task_stage`) → охват CAS должен учитывать и их
(архитектор фиксирует границу).

View File

@@ -0,0 +1,143 @@
---
work_item: ORCH-114
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 02 — ТЗ (TRZ): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные требования к реализации**, выведенные из BRD (`01-brd.md`) и фактического
> кода. **Выбор механизма** (durable lease / heartbeat / transition-epoch / форма хранения, набор
> покрываемых рёбер) и архитектурное обоснование — задача архитектора (`06-adr/`). Здесь — *что*
> должно быть истинно и *какие модули* затрагиваются, не *как* именно.
## 1. Сводка изменения
Вводится **единый инвариант владения** side-effectful переходом стадии: запись стадии и исполнение
тяжёлых под-гейтов/финализации защищаются **durable-механизмом владения** (lease/epoch) + **CAS** на
запись стадии, так что в любой момент переход конкретной задачи доводит **ровно один** актор, а
конкурентный/после-рестартовый повторный вход (reaper / reconciler / webhook / startup-requeue)
**откладывается или становится no-op** вместо повторного применения необратимых эффектов
(merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя / противоречивый rollback↔done).
Аддитивно, под kill-switch, never-raise; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / вердикт-ключи
/ схемы существующих таблиц — не трогаются (обобщает и делает durable процесс-локальный
`finalizer_liveness` ORCH-113).
## 2. Задействованные модули / пути
| Путь | Действие | Назначение в ORCH-114 |
|------|----------|------------------------|
| `src/stage_engine.py` | изменить | `advance_stage`: захват владения на границе side-effectful перехода/финализации; CAS на запись стадии; release в `try/finally`; проигравший — чистый аборт. Покрыть `_handle_merge_verify`, под-гейты `deploy-staging→deploy`, `run_deploy_finalizer` (Phase C), `advance_if_gate_passed` (F-1). |
| `src/db.py` | изменить | CAS-вариант записи стадии (запись только при совпадении ожидаемой текущей стадии/эпохи; rowcount-результат). Durable-хелперы владения (acquire / heartbeat-touch / release / reclaim / snapshot) — форму хранилища задаёт архитектор. |
| `src/finalizer_liveness.py` | изменить/обобщить | Отправная точка: обобщить process-local реестр до **durable, кросс-путевого** владения (или надстроить durable-слой поверх). Сохранить контракт never-raise + kill-switch. |
| `src/job_reaper.py` | изменить | Tier-2/Tier-3 осведомлены о durable-владении на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом, реклейм мёртвого/устаревшего в ограниченное время (NFR-6). |
| `src/queue_worker.py` | изменить | `requeue_running_jobs` / стартовое восстановление сверяется с durable-владением: не пере-исполнять уже применённый необратимый шаг (умное восстановление). `claim_next_job` — не вносить сетевых зависимостей. |
| `src/reconciler.py` | изменить | F-1 (`advance_if_gate_passed`) — defer при активном lease перехода (по образцу skip-guard'ов escalated/Blocked/deps). |
| `src/webhooks/plane.py` | изменить | Пути продвижения (`_try_advance_stage`, Approved / Confirm Deploy) — defer при активном lease. |
| `src/webhooks/gitea.py` | изменить (учесть) | Прямые записи стадии в обход `advance_stage` (`handle_push`/`handle_ci_status`/`handle_pr`) должны попадать под тот же CAS-инвариант либо явно исключаться архитектором (граница в ADR). |
| `src/main.py` | изменить | Порядок старта демонов / точка восстановления; read-only блок в `GET /queue`; опц. operator-эндпоинт реклейма. |
| `src/config.py` | изменить | Kill-switch(и) + бюджеты/таймауты владения + (опц.) CSV-скоуп репо. |
| `tests/test_orch114_transition_ownership.py` | создать | Покрытие FR-1…FR-7 (см. `04-test-plan.yaml`). |
## 3. Функциональные требования
### FR-1 — Единое владение side-effectful переходом (BR-1, BR-6)
На границе, где начинается side-effectful финализация/переход (минимум: под-гейты
`deploy-staging→deploy`, `_handle_merge_verify` на `deploy→done`, Phase C `run_deploy_finalizer`),
актор **захватывает владение** задачей. Пока владение активно, другой актор не исполняет тот же
переход. Release — детерминированно в `try/finally` (в т.ч. на исключении/откате). Владение
**durable** (NFR-4): переживает рестарт и доступно для проверки другому актору/новому процессу.
### FR-2 — Compare-and-swap / epoch на запись стадии (BR-2)
Запись стадии для side-effectful переходов выполняется только если предусловие (ожидаемая текущая
стадия и/или эпоха перехода) не изменилось с момента чтения. Реализуется через CAS-вариант
`update_task_stage` (`UPDATE … SET stage=?[, epoch=epoch+1] WHERE id=? AND stage=?[ AND epoch=?]`,
решение по форме — архитектор). Проигравший гонку писатель получает «lost-race» результат, **не**
мутирует стадию и **не** выполняет ни одного побочного эффекта (merge_pr / ratchet / rebuild /
deploy-init / enqueue следующего агента). Инвариант распространяется и на пути, пишущие стадию в
обход `advance_stage` (gitea-webhook), — либо CAS, либо явное исключение (граница в ADR).
### FR-3 — Reaper, осведомлённый о владении на всех путях (BR-3, NFR-6)
Job-reaper перед реклеймом сверяется с durable-владением **не только** в Tier-2 для `deploy-staging`
(текущая область ORCH-113), а на всех релевантных тирах/рёбрах: **живой** владелец → **defer**
(лог + счётчик, без повторного advance); **мёртвый/устаревший** владелец → реклейм в ограниченное
время (Tier-3 backstop `reaper_max_running_s` добивает зависшего; маркер владения backstop не
обходит инвариант бюджета). Сохранить атомарный `reap_running_job` rowcount-guard.
### FR-4 — Умное восстановление при старте (BR-4, BR-6, NFR-7)
Стартовое восстановление (`requeue_running_jobs` + последующий цикл) использует durable-владение/эпоху,
чтобы **детерминированно** различить: (a) финализация не начиналась / безопасно перезапустить →
re-drive; (b) необратимый шаг уже применён (мерж в `main` / ratchet / прод-деплой инициирован) →
**сойтись к done/консистентному исходу без повторного применения**. Источник истины для «уже
применено» — авторитетные durable-факты (SHA-in-main ORCH-071/073, маркер `INITIATED` self-deploy,
durable-lease/эпоха), а не in-memory состояние.
### FR-5 — Skip/defer в reconciler и webhook (BR-5)
Reconciler F-1 (`advance_if_gate_passed`) и webhook-пути продвижения (`plane._try_advance_stage`,
Approved/Confirm Deploy) при **активном** lease перехода для задачи **откладывают** действие
(silent skip + наблюдаемость), по образцу существующих skip-guard'ов F-1 (escalated / Blocked /
task-deps). Fail-safe: неопределённость состояния lease → консервативный skip (не дублировать).
### FR-6 — Наблюдаемость (BR-7)
Аддитивный read-only блок в `GET /queue` (по образцу `serial_gate`/`merge_gate`/`reaper`):
держатели lease, возраст владения, defer-счётчики, форсированные/устаревшие реклеймы. Алерт
(`send_telegram`, кликабельный номер) на форсированный/устаревший реклейм. Опц. запись в
lessons-journal (ORCH-098, `source="auto"`). Опц. operator-эндпоинт ручного реклейма (по образцу
`POST /serial-gate/unfreeze`).
### FR-7 — Конфигурация, обратимость, never-raise (BR-9, NFR-1, NFR-2, NFR-8)
Все публичные функции владения — never-raise (ошибка → безопасный дефолт + WARNING). Kill-switch
возвращает поведение **байт-в-байт** к до-ORCH-114 (lease не пишется/не читается, CAS вырождается в
прежний безусловный `update_task_stage`). Hot-path — fail-open; prod-safety — fail-closed.
## 4. Изменения API
- **`GET /queue`** — аддитивный read-only блок владения переходом (имя ключа уточнит архитектор,
напр. `transition_ownership` / `transition_lease`). Существующие ключи `/queue` — байт-в-байт.
- **(Опционально, по решению архитектора)** `POST /transition-lease/release?work_item=<id>`
операторский ручной реклейм застрявшего владения (паттерн `POST /serial-gate/unfreeze`).
- `GET /metrics` (ORCH-099) — при необходимости аддитивное поле без бампа `schema_version` (sidecar
обязан толерировать незнакомые ключи). Прочие эндпоинты — не трогаются.
## 5. Изменения схемы БД
**Требование (форму выбирает архитектор, `06-adr/` + `08-data-requirements.md`):** для durable-владения
(NFR-4) и CAS/epoch (FR-2) требуется **аддитивное, идемпотентное** durable-состояние. Кандидаты
(не предписание):
- доп. аддитивная таблица владения (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/
`coverage_baseline`/`lessons`) с `(task_id/job_id, owner, run_id, stage, acquired_at, heartbeat_at,
expires_at)`; **либо**
- аддитивные колонки на `tasks`/`jobs` (`_ensure_column`, паттерн `tasks.track`/`tasks.cancelled_at`),
включая возможную `epoch/version`-колонку для CAS.
**Жёсткие ограничения (NFR-3):** только аддитивно/идемпотентно; **схемы существующих таблиц
(`tasks`/`jobs`/`agent_runs` и пр.) — байт-в-байт**; никаких изменений существующих столбцов/индексов,
ломающих обратную совместимость; restart-safe инициализация в `init_db()`.
## 6. Требования к новым/изменённым QG checks
**Нет.** `QG_CHECKS` / `check_*` / `_parse_*` / машинные вердикт-ключи — **не трогаются**. Владение
переходом — свойство **движка переходов и фоновых акторов**, а **не** Quality Gate и **не** стадия
(аналогично тому, как merge-lease/serial-gate/finalizer-liveness — врезки/leaf'ы, а не QG). Никаких
новых стадий/рёбер в `STAGE_TRANSITIONS`.
## 7. Совместимость / регресс
- **Kill-switch** (новый флаг в `config.py`, env `ORCH_*`): `False` → CAS вырождается в прежний
безусловный `update_task_stage`, lease не пишется/не читается, reaper/reconciler/webhook ведут себя
как до ORCH-114 — **байт-в-байт** (включая зелёный существующий `pytest tests/`).
- **Область раската:** по образцу leaf-гейтов; durable-lease минимально применяется к self-hosting
рёбрам (`deploy-staging`/`deploy→done`, где живут необратимые эффекты); generic-CAS инертен при
отсутствии гонки (нулевая стоимость на не-затронутых переходах). Точную область фиксирует архитектор.
- **Обратимость:** механизм аддитивен и изолирован; откат = выключить kill-switch (durable-таблица/
колонки остаются инертными).
- **never-raise / fail-open / fail-closed:** hot-path claim/guard — fail-open (не клинить общую
очередь, AC-8 ORCH-088); prod-safety/необратимость — fail-closed; любой сбой механизма — WARNING +
безопасный дефолт.
- **Сквозной бюджет:** lease согласован с `reaper_max_running_s`/`reaper_finalize_grace_s` (ORCH-065/
109/110/113) — не удлиняет финализацию за backstop; устаревший владелец добивается в ограниченное время.
- **Маркеры трассировки (ORCH-078):** правки в блоках с маркерами ORCH-065/109/110/111/113 (reaper,
finalizer-liveness, merge-gate) сверяются с их ADR перед изменением; новый код помечается `ORCH-114`.
- **enduro-trails:** при флаге off / репо вне области — нулевая регрессия.

View File

@@ -0,0 +1,177 @@
---
work_item: ORCH-114
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-114 — Ownership-lease для side-effectful переходов + умное восстановление при старте
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
считается провалом). Reviewer/CI проверяет их буквально по файлам и тестам репозитория.
---
## AC-1 — Обязательный регресс: нет двойного эффекта при конкурентном входе в переход
**Условие:** два актора одновременно входят в `advance_stage(deploy-staging)` для одной задачи
(живой монитор-финализатор + второй путь: reaper / reconciler F-1 / webhook). Тест-двойники для
`merge_pr` / coverage-ratchet / image-rebuild / deploy-init считают число вызовов.
- **PASS:** side-effectful шаги (merge_pr, ratchet baseline, image-rebuild, инициация прод-деплоя,
enqueue следующего агента) выполняются **ровно один раз**; персистится **ровно один** согласованный
исход стадии; второй актор получает «lost-race»/defer и **не** выполняет побочных эффектов. Тест
**красный до фикса, зелёный после** (воспроизводит класс инцидента ORCH-111).
- **FAIL:** любой side-effectful шаг вызван дважды; либо два противоречивых исхода (один откатил на
`development`, другой довёл до `done`); либо тест не воспроизводит проблему до фикса.
---
## AC-2 — Compare-and-swap на запись стадии
**Условие:** CAS-вариант записи стадии вызывается двумя писателями с одинаковым ожидаемым предусловием;
первый применяется, второй приходит со «устаревшим» ожиданием.
- **PASS:** первый writer применяет переход (rowcount=1); второй получает «lost-race» (rowcount=0),
стадия **не** мутируется повторно; при выключенном kill-switch CAS вырождается в прежний безусловный
`update_task_stage` (байт-в-байт).
- **FAIL:** второй writer перезатирает стадию; либо CAS меняет семантику записи при выключенном флаге.
---
## AC-3 — Жизненный цикл владения: acquire / release / реклейм
**Условие:** актор начинает side-effectful финализацию.
- **PASS:** владение захватывается на границе финализации и освобождается в `try/finally` — в т.ч.
при исключении и при откате (rollback); durable-запись владения видна другому актору; после release
владение реклеймится/свободно.
- **FAIL:** владение не освобождается при исключении/откате (lease «течёт» и клинит задачу); либо не
захватывается на границе.
---
## AC-4 — Reaper откладывает реклейм при живом владении (все пути, не только Tier-2/deploy-staging)
**Условие:** durable-владение активно (живой финализатор), reaper делает тик.
- **PASS:** reaper **defer** (лог + счётчик, без повторного `advance_stage`) пока владение живо и в
пределах бюджета; область defer обобщена за пределы Tier-2/`deploy-staging` ORCH-113 на релевантные
пути; атомарный `reap_running_job` rowcount-guard сохранён.
- **FAIL:** reaper повторно исполняет финализацию при живом владельце; либо defer ограничен только
прежней узкой областью, оставляя кросс-путь открытым.
---
## AC-5 — Reaper добивает мёртвое/устаревшее владение в ограниченное время
**Условие:** владелец провально мёртв/завис (lease устарел), финализация не прогрессирует.
- **PASS:** reaper реклеймит задачу в пределах Tier-3 backstop `reaper_max_running_s` (маркер владения
backstop не обходит); задача не остаётся навсегда заклиненной; сквозной инвариант
`reaper_max_running_s > Σ(deploy-staging gate-work) + grace` сохранён.
- **FAIL:** мёртвое владение блокирует задачу бессрочно; либо нарушен бюджетный инвариант ORCH-065/
109/110/113.
---
## AC-6 — Умное восстановление при рестарте процесса
**Условие:** процесс убит **в середине** финализации `deploy-staging`/`deploy` (in-memory состояние
потеряно); процесс перезапущен (`requeue_running_jobs` + цикл).
- **PASS:** восстановление через durable-владение/эпоху + авторитетные факты (SHA-in-main ORCH-071/073,
маркер `INITIATED`) детерминированно сходится к **единственному** согласованному исходу: незавершённое
дорешается, **уже применённый** необратимый шаг (мерж/ratchet/прод-деплой) **не** применяется повторно.
- **FAIL:** после рестарта переход исполняется заново с двойным необратимым эффектом; либо возникает
противоречие rollback↔done; либо задача застревает нетерминальной с удержанным lease.
---
## AC-7 — Reconciler F-1 пропускает переход при активном lease
**Условие:** lease перехода активен; reconciler F-1 сканирует задачу.
- **PASS:** F-1 **defer/skip** (silent, наблюдаемо), не вызывает `advance_stage`, по образцу skip-guard'ов
escalated/Blocked/task-deps; fail-safe: неопределённость lease → консервативный skip.
- **FAIL:** F-1 продвигает стадию параллельно живому владельцу.
---
## AC-8 — Webhook-путь пропускает переход при активном lease
**Условие:** lease перехода активен; приходит Plane-webhook (Approved / Confirm Deploy) на ту же задачу.
- **PASS:** webhook-путь продвижения **defer**, не дублирует переход/финализацию при живом владельце;
поздний легитимный сигнал не теряется (повторно отработает после release или станет идемпотентным no-op).
- **FAIL:** webhook повторно входит в переход параллельно владельцу и даёт двойной эффект.
---
## AC-9 — Kill-switch off → поведение байт-в-байт прежнее
**Условие:** новый kill-switch выключен.
- **PASS:** lease не пишется/не читается; CAS вырождается в прежний безусловный `update_task_stage`;
reaper/reconciler/webhook/startup ведут себя как до ORCH-114; существующий `pytest tests/` зелёный
без правок ожиданий; enduro не затронут.
- **FAIL:** при выключенном флаге наблюдается любое отличие от до-ORCH-114 поведения.
---
## AC-10 — never-raise + fail-open (hot-path) / fail-closed (prod-safety)
**Условие:** механизм владения сталкивается с ошибкой (БД-сбой/повреждённая запись lease/исключение).
- **PASS:** ни одна публичная функция владения не роняет конвейер; горячий путь claim/guard —
**fail-open** (общая очередь всех проектов не клинится); решения, критичные для необратимости/прода —
**fail-closed**; на ошибке — WARNING + безопасный дефолт.
- **FAIL:** ошибка механизма роняет claim/конвейер; либо hot-path заклинивает очередь; либо
prod-критичное решение фейлит «открыто».
---
## AC-11 — Инварианты конвейера не тронуты
**Условие:** аудит диффа против `src/stages.py`, `src/qg/checks.py`, схемы БД.
- **PASS:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / машинные
вердикт-ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`/
`coverage_status:`) — **байт-в-байт**; любое новое хранилище аддитивно/идемпотентно
(`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), схемы существующих таблиц неизменны.
- **FAIL:** изменены состав/порядок стадий, реестр/семантика гейтов, вердикт-ключи или существующие
столбцы/индексы.
---
## AC-12 — Наблюдаемость
**Условие:** `GET /queue` при включённом флаге.
- **PASS:** присутствует аддитивный read-only блок владения (держатели/возраст/defer-счётчики/реклеймы);
существующие ключи `/queue` не сломаны; форсированный/устаревший реклейм даёт Telegram-алерт с
кликабельным номером.
- **FAIL:** блок отсутствует/ломает существующий вывод; реклейм происходит молча без наблюдаемости.
---
## AC-13 — Self-hosting безопасность
**Условие:** аудит механизма владения на побочные действия.
- **PASS:** механизм **никогда** не рестартит прод-контейнер `orchestrator`, не пушит/force-push в
`main`, не трогает detached deploy-процесс; деплой орка по-прежнему только через staging-гейт (8501)
и `Confirm Deploy`.
- **FAIL:** механизм владения инициирует рестарт прода/мутацию `main`/вмешательство в detached-деплой.
---
## Сводная матрица AC ↔ FR/BR/NFR
| AC | Покрывает | Тип проверки |
|----|-----------|--------------|
| AC-1 | BR-1, BR-6, BR-8 / FR-1, FR-2 | integration (regression, red→green) |
| AC-2 | BR-2 / FR-2 | unit |
| AC-3 | BR-1 / FR-1 | unit |
| AC-4 | BR-3 / FR-3 | integration |
| AC-5 | BR-3, NFR-6 / FR-3 | unit/integration |
| AC-6 | BR-4, BR-6, NFR-4, NFR-7 / FR-1…FR-4 | integration (restart-recovery) |
| AC-7 | BR-5 / FR-5 | unit/integration |
| AC-8 | BR-5 / FR-5 | unit/integration |
| AC-9 | BR-9, NFR-8 / FR-7 | regression (kill-switch off) |
| AC-10 | NFR-1 / FR-7 | unit |
| AC-11 | NFR-3 / FR-6 (negative) | structural audit |
| AC-12 | BR-7 / FR-6 | unit/integration |
| AC-13 | NFR-5 / FR-1 | structural audit |

View File

@@ -0,0 +1,107 @@
work_item: ORCH-114
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
title: "Ownership-lease для side-effectful переходов стадий + умное восстановление при старте"
framework: pytest
scope: >
Покрывается: единое владение side-effectful переходом (FR-1), CAS/epoch на запись стадии (FR-2),
осведомлённость reaper о живом/мёртвом владении на всех путях (FR-3), умное восстановление при
рестарте (FR-4), skip/defer в reconciler F-1 и webhook (FR-5), наблюдаемость (FR-6), kill-switch
и never-raise (FR-7). Вне покрытия: переход на uvicorn --workers>1, частные симптомы ORCH-110/112
(уже закрыты и переиспользуются), изменение STAGE_TRANSITIONS/QG_CHECKS/check_*.
notes: >
TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс класса инцидента ORCH-111: красный до фикса, зелёный после.
Все side-effectful вызовы (merge_pr / coverage-ratchet / image-rebuild / deploy-init) проверяются
через тест-двойники со счётчиком вызовов — без сети/реального git/прода/ssh. Restart-recovery
моделируется сбросом in-memory состояния + повторным прогоном стартового восстановления над durable
состоянием БД. Полный регресс tests/ должен оставаться зелёным; при выключенном kill-switch
поведение байт-в-байт прежнее.
tests:
- id: TC-01
type: integration
description: "ОБЯЗАТЕЛЬНЫЙ РЕГРЕСС. Два конкурентных входа в advance_stage(deploy-staging) одной задачи (живой финализатор + reaper/reconciler/webhook): каждый side-effectful шаг (merge_pr/ratchet/rebuild/deploy-init/enqueue) вызывается ровно один раз; персистится один согласованный исход; второй актор — lost-race/defer без побочных эффектов. Красный до фикса, зелёный после."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-02
type: unit
description: "CAS-запись стадии: первый writer применяет (rowcount=1), второй с устаревшим предусловием получает lost-race (rowcount=0) и не мутирует стадию (FR-2/AC-2)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-03
type: unit
description: "Жизненный цикл владения: acquire на границе финализации; release в try/finally при нормальном завершении, при исключении и при откате (lease не течёт); durable-запись видна другому актору (FR-1/AC-3)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-04
type: integration
description: "Reaper defer при живом владении на путях за пределами Tier-2/deploy-staging ORCH-113: повторный advance не выполняется, атомарный reap_running_job rowcount-guard сохранён, ведётся счётчик defer (FR-3/AC-4)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-05
type: unit
description: "Reaper добивает мёртвое/устаревшее владение в пределах Tier-3 backstop reaper_max_running_s; маркер владения backstop не обходит; задача не клинится бессрочно (FR-3/AC-5/NFR-6)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-06
type: integration
description: "Умное восстановление при рестарте: процесс убит в середине финализации, in-memory сброшен; стартовое восстановление над durable-состоянием + авторитетными фактами (SHA-in-main, INITIATED) сходится к единственному исходу без повторного необратимого эффекта (FR-4/AC-6)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-07
type: integration
description: "Reconciler F-1 (advance_if_gate_passed) делает defer/skip при активном lease перехода; fail-safe: неопределённость lease → консервативный skip (FR-5/AC-7)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-08
type: integration
description: "Webhook-путь (plane._try_advance_stage, Approved/Confirm Deploy) делает defer при активном lease; поздний легитимный сигнал не теряется (повтор после release / идемпотентный no-op) (FR-5/AC-8)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-09
type: integration
description: "Kill-switch off: lease не пишется/не читается, CAS вырождается в прежний безусловный update_task_stage, reaper/reconciler/webhook/startup — байт-в-байт до ORCH-114; существующий pytest tests/ зелёный (FR-7/AC-9/NFR-8)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-10
type: unit
description: "never-raise + fail-open/fail-closed: ошибка/повреждённая запись lease/исключение БД не роняют конвейер; hot-path claim/guard fail-open; prod-safety решение fail-closed; WARNING + безопасный дефолт (FR-7/AC-10/NFR-1)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-11
type: unit
description: "Структурный аудит инвариантов: STAGE_TRANSITIONS / QG_CHECKS / имена-семантика check_* / вердикт-ключи байт-в-байт; новое хранилище аддитивно/идемпотентно (CREATE TABLE IF NOT EXISTS / _ensure_column), схемы существующих таблиц неизменны (NFR-3/AC-11)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-12
type: integration
description: "Наблюдаемость: GET /queue несёт аддитивный read-only блок владения (держатели/возраст/defer/реклеймы), существующие ключи не сломаны; форсированный/устаревший реклейм даёт Telegram-алерт с кликабельным номером (FR-6/AC-12)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-13
type: unit
description: "Self-hosting безопасность: механизм владения не инициирует рестарт прод-контейнера, не пушит/force-push в main, не трогает detached deploy-процесс (NFR-5/AC-13)."
module: tests/test_orch114_transition_ownership.py
expected: PASS
- id: TC-14
type: integration
description: "Полный регресс конвейера: pytest tests/ остаётся зелёным; deploy-staging-ребро и deploy-finalizer (Phase C) проходят при включённом механизме без двойных эффектов в одно-акторном happy-path (BR-8)."
module: tests/
expected: PASS

View File

@@ -0,0 +1,300 @@
---
work_item: ORCH-114
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---
# ADR-001: Durable transition-ownership lease + expected-stage CAS для side-effectful переходов стадий
Work Item: **ORCH-114** — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`** (решение кросс-каттинговое: новый durable-механизм владения трогает движок переходов и ≥5 фоновых акторов).
## Статус
Proposed
## Контекст
ORCH-114 — **системный наследник** инцидент-цепочки ORCH-110/111/112/113. Каждый предшественник
закрыл точечный симптом, но **корневой класс остался открыт: у side-effectful переходов стадий нет
единого владения**. Факты сверены по коду:
- **Запись стадии не атомарна по предусловию.** `db.update_task_stage` (`src/db.py:671679`) —
голый `UPDATE tasks SET stage=?, updated_at=… WHERE id=?` **без** `WHERE stage=?` (нет CAS, нет
epoch/version-колонки). Второй вызов безусловно перезатирает первый.
- **`advance_stage` ре-ентерабельна без защиты** (`src/stage_engine.py:176507`). Внутри нет ни
in-memory-лока на `task_id`, ни durable-маркера «переход идёт». `current_stage` читается на входе
(параметр), тяжёлые под-гейты ребра `deploy-staging → deploy` (security → merge-gate re-test →
coverage → image-freshness, **минуты**) и `_handle_merge_verify` (`deploy → done`: `merge_pr`,
ratchet baseline, proof-of-merge) исполняются **до** единственной записи стадии на `:402`. Два
конкурентных вызова оба читают `deploy-staging`, оба гоняют ВСЕ под-гейты, оба пишут `deploy`.
- **Минимум 5 путей входят в переход независимо:** монитор (`launcher._try_advance_stage`),
Plane-webhook (`plane._try_advance_stage:865`), reconciler F-1 (`advance_if_gate_passed →
advance_stage`, `stage_engine.py:573`), job-reaper (`_gate_driven_advance → launcher._try_advance_stage`,
`job_reaper.py:406`), deploy-finalizer Phase C (`run_deploy_finalizer → advance_stage`,
`stage_engine.py:1980`). Ни один не проверяет, не в этом ли переходе уже другой актор.
- **6 путей пишут стадию в ОБХОД `advance_stage`** прямым `update_task_stage` (риск BRD §8): 5 в
`webhooks/gitea.py` (`:127` arch→dev по ADR-push, `:242` dev→review по CI-green, `:333` review→testing
по PR-approved, `:359` review→dev по REQUEST_CHANGES, `:398` *→done по PR-merge) и 1 в
`webhooks/plane.py:806` (rollback Rejected).
- **ORCH-113 (`finalizer_liveness`, adr-0043)** — отправная точка, но: **process-local in-memory**,
**только reaper Tier-2**, **только `deploy-staging`**, **теряется при рестарте**. Остаточный кросс-путь
(живой монитор внутри `advance_stage(deploy-staging)` + reaper после рестарта / reconciler F-1 /
webhook) повторно входит в тот же переход → двойные эффекты и противоречие rollback↔done (инцидент
ORCH-111, job 1914 / PR #130).
**Решающий факт, проверенный по коду:** механизм владения по pid уже существует и испытан — merge-lease
(`merge_gate.py`) штампит `os.getpid()` (`:360`) в lease-файл и реклеймит держателя по `pid_alive`
(`:452,526`); reaper Tier-1 тоже судит по `pid_alive` (`job_reaper.py:245`). ORCH-114 строит durable
владение **тем же испытанным pid-приёмом**, а не вводит новый таймер (таймер был источником бага
ORCH-111).
## Решение
### Сводка
Вводятся **два комплементарных слоя**, оба аддитивные, под единым kill-switch, never-raise:
1. **Durable transition-lease** (владение на **входе** в side-effectful регион) — новая аддитивная
таблица `transition_lease` (`src/db.py`, паттерн `repo_freeze`/`coverage_baseline`). Актор
**захватывает** владение задачей перед тяжёлой финализацией; второй актор, увидев живого владельца,
**не стартует** под-гейты вовсе. Это и убивает класс двойного эффекта (предотвращение, а не починка
постфактум). Release — в `try/finally`. **Liveness владельца = `owner_pid` + `owner_boot_id`** (НЕ
heartbeat), что делает рестарт-recovery бесплатным (boot-id новый ⇒ старые lease мертвы) и не
страдает от блокирующего re-test.
2. **Expected-stage CAS** (корректность на **коммите** записи стадии) — CAS-вариант
`update_task_stage_cas(task_id, expected_stage, new_stage)`
`UPDATE tasks SET stage=?, updated_at=… WHERE id=? AND stage=?`, rowcount==1 ⇒ выиграл, 0 ⇒
проиграл гонку → **аборт без побочных эффектов**. Покрывает узкое остаточное окно гонки И 6 путей
в обход `advance_stage`.
Слой 1 гарантирует «двое не начнут»; слой 2 гарантирует «даже если начали — запишет один». Defense in
depth. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/вердикт-ключи/схемы существующих таблиц — байт-в-байт.
### D1 — Механизм: durable-lease (вход) + CAS (коммит), оба обязательны (FR-1, FR-2 / BR-1, BR-2, BR-6)
Почему **оба**, а не один:
- **CAS-only недостаточно.** CAS стоит на записи стадии — в *конце* `advance_stage` (`:402`). К этому
моменту проигравший актор **уже исполнил** `merge_pr` / docker-rebuild / re-test. CAS на коммите не
предотвращает двойной побочный эффект *в полёте*. → нужен lease на **входе** в регион.
- **Lease-only недостаточно** для 6 путей в обход `advance_stage` (gitea/plane прямой
`update_task_stage`) и для остаточного окна между «consult lease» и «acquire». → нужен CAS как
backstop записи.
Lease — это owner-эксклюзия; CAS — это атомарность-записи. Они ортогональны и складываются.
### D2 — Форма хранения: новая таблица `transition_lease`, без новых колонок на `tasks`/`jobs` (NFR-3, NFR-4)
Durable-владение хранится в **новой аддитивной таблице** (`CREATE TABLE IF NOT EXISTS`, паттерн
`repo_freeze`/`coverage_baseline`/`lessons`), а **не** в колонках `tasks`/`jobs`. Это **усиливает**
NFR-3: схемы существующих таблиц остаются буквально байт-в-байт; добавляется ровно один объект.
```sql
CREATE TABLE IF NOT EXISTS transition_lease (
task_id INTEGER PRIMARY KEY, -- одна задача = ≤1 активный владелец перехода
owner TEXT NOT NULL, -- актор: monitor|reaper|reconciler|webhook|finalizer
owner_pid INTEGER, -- pid процесса-держателя (как merge-lease)
owner_boot_id TEXT, -- нонс старта процесса (рестарт ⇒ смена ⇒ старый lease мёртв)
run_id INTEGER, -- agent_runs.id, если применимо
stage TEXT, -- from-стадия, на которой захвачено (контекст/наблюдаемость)
acquired_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
**CAS на запись стадии — через предикат ожидаемой стадии, без epoch-колонки.** В текущей одно-процессной
модели каждое side-effectful ребро ведёт в **отличную** стадию, поэтому `WHERE id=? AND stage=?`
полный и корректный compare-and-swap (стадия *и есть* версия). Отдельная `epoch/version`-колонка была бы
неиспользуемой машинерией → отвергнута; задокументирована как форвард-расширение под будущий
`--workers>1` (если появится same-stage ре-ентерабельность). Это решает FR-2 «ожидаемая текущая стадия
**и/или** эпоха» в пользу стадии.
### D3 — Liveness владельца = pid + boot-id, НЕ heartbeat (NFR-4, NFR-6)
Владелец считается **живым**`owner_boot_id == <текущий boot-id процесса>` **И**
`merge_gate.pid_alive(owner_pid)`. Иначе lease **устарел** → реклеймится.
Почему не heartbeat: ORCH-113 (adr-0043, раздел «Альтернативы») **сам отверг** durable-heartbeat
доводом «блокирующий re-test не может бить heartbeat» — `merge_retest_timeout_s=900` синхронно держит
поток монитора, heartbeat с коротким окном дал бы ложное «мёртв». pid-liveness свободна от этого: процесс
жив весь re-test → lease жив; **никакого heartbeat-кода в блокирующей финализации**.
- **Рестарт (self-deploy):** новый процесс имеет новый `boot_id` → все ранее записанные lease мгновенно
«мертвы» (boot-id mismatch) → реклеймятся → requeued job (после `requeue_running_jobs`) переисполняет
переход идемпотентно (D7). Это durable-аналог «in-memory реестр обнуляется на рестарте» (на чём держится
ORCH-113), но **переживает** рестарт как durable-запись для проверки другим актором/тиром.
- **Реальная смерть pid в том же процессе:** `pid_alive` False → реклейм немедленно (как reaper Tier-1).
- **Живой, но зависший владелец (pid жив, не прогрессирует):** добивается **Tier-3 backstop**
`reaper_max_running_s` (ниже, D8) — ограниченное время, маркер владения backstop **не обходит**.
### D4 — Область охвата: lease на side-effectful рёбрах, CAS — на всех записях стадии + 6 обходных путях
| Путь записи стадии | Lease | CAS | Обоснование |
|--------------------|:-----:|:---:|-------------|
| `deploy-staging → deploy` под-гейты (`stage_engine.py:321402`) | **да** | да | необратимо: merge re-test/rebuild/инициация |
| `deploy → done` `_handle_merge_verify` (`:397402,1726`) | **да** | да | необратимо: `merge_pr`, ratchet baseline |
| Phase C `run_deploy_finalizer` (`:18982009`) | **да** | да | необратимо: прод-деплой/мерж |
| прочие рёбра `advance_stage` (created→…→testing) | нет | да | обратимы; CAS инертен без гонки |
| rollback-записи `_handle_*_rollback` (`:740…1422`) | нет¹ | да | защита от rollback↔done (BR-6) |
| 5× `gitea.py` прямой `update_task_stage` | нет | **да** | закрыть обход (BRD §8) |
| 1× `plane.py:806` rollback | нет | **да** | закрыть обход (BRD §8) |
¹ rollback исполняет тот же единственный владелец lease (он держит lease на входе в регион), поэтому
отдельный lease на rollback-запись не нужен — достаточно CAS.
**Граница (фиксируется здесь):** обходные пути gitea/plane получают **CAS** (дёшево, закрывает дыру
BRD §8), но **не** полный lease — они не исполняют необратимых шагов (только enqueue агента/флип
индикативной стадии). CAS не даёт им перетереть авторитетную запись владельца.
### D5 — Интеграция в `advance_stage` (FR-1, FR-2, AC-1, AC-3)
```
advance_stage(...):
if transition_lease.applies(repo) and <ребро side-effectful>:
if not transition_lease.acquire(task_id, owner, run_id, current_stage):
return AdvanceResult(advanced=False, note="transition-lease-busy") # чистый аборт
try:
<под-гейты / _handle_merge_verify / финализация — как сейчас>
# запись стадии — через CAS:
if not update_task_stage_cas(task_id, current_stage, next_stage):
return AdvanceResult(advanced=False, note="stage-cas-lost") # без побочных эффектов
<enqueue next agent, notify, …>
finally:
transition_lease.release(task_id, owner) # в т.ч. на исключении/откате (AC-3)
```
Проигравший acquire или CAS — **не** мутирует стадию и **не** исполняет ни одного side-effect. Release
гарантирован `finally` (lease «не течёт» на исключении/rollback). Когда kill-switch off — `acquire`
no-op→True, CAS вырождается в прежний безусловный `update_task_stage` → байт-в-байт (D9, AC-9).
### D6 — Reaper / reconciler / webhook / startup осведомлены о владении (FR-3, FR-5, BR-3, BR-5)
- **Reaper** (`job_reaper.py`): перед реклеймом/re-drive консультирует durable-lease на **всех**
релевантных путях (обобщение ORCH-113 за пределы Tier-2/`deploy-staging`): **живой** владелец → defer
(лог + счётчик); **мёртвый/устаревший** → реклейм. Tier-3 (`reaper_max_running_s`) маркер **игнорирует**
(добивает зависшего). Атомарный `reap_running_job` rowcount-guard сохранён. **Реклейм/реап освобождает
lease задачи** (force) — lease не переживает реап.
- **Reconciler F-1** (`reconciler.py`, перед `advance_if_gate_passed` на `:249`): новый skip-guard по
образцу escalated/Blocked/task-deps — активный живой lease → silent defer.
- **Webhook** (`plane.py` Approved/`:413` + Confirm Deploy/`:219`): активный живой lease → defer; поздний
легитимный сигнал отработает после release или станет идемпотентным no-op.
- **`finalizer_liveness` (ORCH-113) сохраняется без правок** как поведение при **выключенном** ORCH-114
(надстройка durable-слоя поверх, TRZ §2): kill-switch off ⇒ reaper консультирует in-memory
`finalizer_liveness` (Tier-2/`deploy-staging`, ровно ORCH-113); kill-switch on ⇒ reaper консультирует
durable `transition_lease` (cross-path). Так ORCH-114 **обобщает** ORCH-113, не ломая его контракт/тест.
### D7 — Умное восстановление = stale-clear + авторитетные факты, БЕЗ нового «recovery-мозга» (FR-4, BR-4, BR-6, NFR-7)
Ключевое архитектурное решение, снимающее риск BRD §8 («некорректный smart-recovery сам станет источником
двойного применения»): ORCH-114 **не строит** новую машину восстановления. Восстановление = композиция:
1. **`requeue_running_jobs()`** (существует, `db.py:1320`; в `main.lifespan` до старта reaper) — running→queued.
2. **`transition_lease.recover_on_startup()`** — boot-id новый ⇒ все ранее записанные lease мертвы;
reaper/claim их реклеймят (наблюдаемо: лог + алерт на форсированный реклейм).
3. **Идемпотентность re-drive — уже обеспечена авторитетными durable-фактами предшественников**, lease их
не дублирует, а лишь гарантирует **последовательную** (не конкурентную) их проверку:
- **SHA-in-main** (ORCH-071/073/093): `merge_gate.verify_merged_to_main` / `ensure_open_pr →
"already-in-main"` → повторный `_handle_merge_verify` доводит до `done` **без** второго `merge_pr`.
- **Маркер `INITIATED`** (self-deploy ORCH-036): Phase B idempotency-guard (`stage_engine.py:1567`) →
повторный заход не инициирует второй прод-деплой.
- **Coverage-ratchet CAS** (ORCH-027): `ratchet_coverage_baseline` (`UPDATE … WHERE coverage<=?`) —
повторный ratchet идемпотентен по построению.
Итог: после смерти процесса в середине финализации система сходится к **единственному** исходу —
незавершённое дорешается, уже применённый необратимый шаг **не** повторяется (источник истины «уже
применено» = авторитетные факты, не in-memory).
### D8 — Сквозной бюджет reaper: без новых таймаутов (NFR-6)
Lease **не вводит** собственный долгий TTL. Его жёсткий потолок возраста **совпадает** с Tier-3
`reaper_max_running_s` (5400): reaper при реапе job на Tier-3 force-освобождает lease — lease и job
реклеймятся в один момент. Раннее обнаружение смерти — через pid+boot (D3), а не таймер. Поэтому
сквозной инвариант ORCH-065/109/110/113 `reaper_max_running_s (5400) > Σ(deploy-staging gate-work ≈4460)
+ grace` **остаётся нетронутым**, `reaper_finalize_grace_s`/`reaper_max_running_s` **не меняются**. Новых
бюджетных констант, требующих согласования, нет.
### D9 — never-raise / fail-open / fail-closed / kill-switch (FR-7, NFR-1, NFR-8, AC-9, AC-10)
- **Hot-path `claim_next_job` НЕ трогается** — lease консультируется на пути перехода/финализации и в
reaper/reconciler/webhook, **не** в claim. → общая очередь всех проектов не может заклиниться на баге
lease (fail-open по построению, AC-8 ORCH-088 цел).
- **acquire/guard-ошибка** (БД-сбой/повреждённая строка): на side-effectful пути → консервативный
**defer/abort текущей попытки без побочных эффектов** (fail-closed к недвоению; не вечный клин — следующий
тик/reaper переисполнит, в пределе Tier-3 добьёт). guard reconciler/webhook → консервативный skip.
- **CAS-ошибка** → аборт записи (не слепой write).
- **Kill-switch `transition_lease_enabled=False`** → lease не пишется/не читается; CAS вырождается в
прежний `update_task_stage`; reaper → ORCH-113 fallback; reconciler/webhook skip-guard инертен → **байт-в-байт**
до-ORCH-114 (зелёный существующий `pytest tests/` без правок ожиданий).
### D10 — Наблюдаемость и конфигурация (FR-6, BR-7, NFR-2, AC-12)
- `GET /queue` — аддитивный read-only блок `transition_lease` (держатели/owner/stage/возраст/defer-счётчики/
форсированные/устаревшие реклеймы); существующие ключи не тронуты.
- **Telegram-алерт** (`send_telegram`, кликабельный номер) на форсированный/устаревший реклейм.
- **Опц.** `POST /transition-lease/release?work_item=<id>` — операторский ручной реклейм (паттерн
`POST /serial-gate/unfreeze`).
- **Опц.** lessons-journal автозапись (ORCH-098, `source="auto"`) на форсированный реклейм.
- **Флаги** (`config.py`): `transition_lease_enabled: bool = True` (env `ORCH_TRANSITION_LEASE_ENABLED`,
kill-switch); `transition_lease_repos: str = ""` (CSV; **пусто → self-hosting only**, паттерн
`coverage_gate_repos`/`serial_gate_repos` → enduro не затронут). Новых таймаутов нет (D8).
- **Leaf `src/transition_lease.py`** (never-raise, паттерн `serial_gate`/`coverage_gate`/`finalizer_liveness`):
`applies(repo)` / `acquire(task_id, owner, run_id, stage) -> bool` / `is_held_by_live_owner(task_id) -> bool`
/ `release(task_id, owner=None)` / `reclaim_if_stale(task_id) -> bool` / `recover_on_startup()` / `snapshot()`.
## Альтернативы
- **Только CAS/epoch, без lease** — отвергнуто: CAS на коммите не предотвращает двойной side-effect
*в полёте* (re-test/rebuild/merge исполняются до записи стадии). Не закрывает класс ORCH-111.
- **Только durable-lease, без CAS** — отвергнуто: не покрывает 6 путей в обход `advance_stage` и узкое
окно «consult→acquire».
- **Heartbeat-liveness** — отвергнуто доводом самого ORCH-113: блокирующий 900s re-test не может бить
heartbeat → ложное «мёртв». pid+boot свободна от этого.
- **Lease-файл per-task** (клон merge-lease) — отвергнуто: CAS на запись стадии — DB-операция; держать
владение в той же транзакционной БД когерентнее и позволяет атомарный acquire тем же rowcount-guard
идиомом (`claim_next_job`/`reap_running_job`), что код уже доверяет. merge-lease-файл остаётся per-**repo**
для **другой** задачи (сериализация мержей между задачами репо) — не дублируется.
- **`epoch/version`-колонка на `tasks`** — отвергнуто (для текущей модели): стадия *и есть* версия для
side-effectful рёбер; колонка была бы неиспользуемой. Задокументирована как форвард-расширение.
- **Sub-state `finalizing` в `jobs.status`** — отвергнуто (как в ORCH-113): меняет семантику статуса для
claim/requeue/reconciler/reaper — нарушение NFR-3.
- **Per-stage grace, покрывающая Σ финализации** — отвергнуто (как в ORCH-113): нарушает бюджет
`5400 > Σ+grace`; таймер = источник бага.
- **Бесшовно для всех репо (без self-hosting-скоупа)** — отвергнуто: по образцу coverage/serial-gate
область по умолчанию self-hosting (необратимые эффекты живут на self-hosting-рёбрах); enduro инертен.
- **Новый бесшовный «recovery-мозг»** — отвергнуто (BRD §8 риск): композиция requeue + stale-clear +
авторитетные факты (D7) проще и не вносит нового источника двойного применения.
## Последствия
- **+** Класс двойного эффекта/противоречия rollback↔done закрыт **в корне** (предотвращение на входе),
а не починкой постфактум; покрыты конкурентный, reaper-после-рестарта, reconciler и webhook пути.
- **+** Рестарт-safe без нового таймера и без переписывания на multi-process (boot-id готовит почву под
будущий `--workers>1`, NFR-4).
- **+** Сквозной бюджет reaper и все инварианты конвейера (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/
вердикт-ключи) не тронуты; схемы существующих таблиц байт-в-байт; +1 аддитивная таблица.
- **+** Дыра обходных путей gitea/plane (BRD §8) закрыта CAS.
- **** Гарантия эксклюзии валидна при одном процессе/одной БД (как ORCH-113); durable-lease лишь делает
её **корректной** и для рестарта/будущей multi-process — но полноценная multi-writer верификация — вне
объёма (риск TR-6 в `10-tech-risks.md`).
- **** Узкое окно «штамп `finished_at` → acquire» (как ORCH-113) маркером не покрыто — закрыто прежним
grace=300 + CAS-backstop.
- **Откат:** `ORCH_TRANSITION_LEASE_ENABLED=false` → байт-в-байт до-ORCH-114 (таблица остаётся инертной).
## Ссылки
- BRD: `docs/work-items/ORCH-114/01-brd.md`
- TRZ: `docs/work-items/ORCH-114/02-trz.md`
- Acceptance: `docs/work-items/ORCH-114/03-acceptance-criteria.md`
- Данные: `docs/work-items/ORCH-114/08-data-requirements.md`
- Риски: `docs/work-items/ORCH-114/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`
- Предшественники: `adr-0043` (ORCH-113 finalizer-liveness), `adr-0042` (ORCH-110 merge-retest),
`adr-0027`/`merge-lease` (ORCH-043), `adr-0040` (ORCH-109 бюджеты), `adr-0011` (ORCH-065 reaper),
`adr-0029` (ORCH-027 coverage-ratchet)
- Сверено по коду: `src/db.py:671679,13201335,14641505,9881055`, `src/stage_engine.py:176507,17262009`,
`src/finalizer_liveness.py`, `src/job_reaper.py:245,406,436461`, `src/reconciler.py:249,515575`,
`src/webhooks/plane.py:219,413,806`, `src/webhooks/gitea.py:127,242,333,359,398`,
`src/merge_gate.py:311411,452,526`
</content>
</invoke>

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