/
kochenov
/
uni
Обзор
Документация
Войти
/
kochenov
/
uni
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
scripts/run_phase.py
1 667 строк
97 KB
Dmitry Kochenov
Initial commit: UMP base structure
16 июл 2026, 21:14
16 июл 2026, 21:14
86b4d0e
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ Универсальный исполнитель фаз шага (аудит v12). ЗАМЕЩАЕТ субагентов для механических фаз. Агент вызывает этот скрипт, а не пытается прочитать 43KB промпт в контекст субагента. Фазы, которые скрипт выполняет ПОЛНОСТЬЮ (без участия агента): PREFLIGHT — создать ветку, проверить контекст, обновить STEP_STATE TESTING — запустить verify, записать результаты в docs_target PHASE2B — проверить сохраняемые секции FINALIZATION — вызвать finalize_step.py Фазы, где скрипт делает ПОДГОТОВКУ, а агент — ТВОРЧЕСКУЮ работу: PHASE1 — скрипт создаёт шаблон docs_target, агент заполняет секции IMPLEMENTATION — скрипт подготавливает окружение, агент пишет код PHASE2A — скрипт отмечает секции для переписывания, агент переписывает АВТО-ЛОГИРОВАНИЕ: все действия автоматически пишутся в _meta/agent.log Запуск: uv run python scripts/run_phase.py <step_id> <phase> uv run python scripts/run_phase.py 1.0 PREFLIGHT uv run python scripts/run_phase.py 1.0 PHASE1 uv run python scripts/run_phase.py 1.0 TESTING Exit codes: 0 — фаза выполнена 1 — ошибка 2 — шаг не найден 3 — precondition не пройден """ import argparse import json import os import re import subprocess from subprocess import PIPE import sys import tempfile from datetime import datetime from pathlib import Path import yaml BASE = Path(__file__).parent.parent PLANS_DIR = BASE / 'plans' ERRORS_FILE = BASE / '_meta' / 'errors.json' sys.path.insert(0, str(BASE / 'scripts')) from agent_log import add_entry # ============================================================================ # FIX-1.11: атомарная запись файла через temp + os.replace (POSIX) # ============================================================================ def atomic_write_text(path: Path, content: str) -> None: """Атомарная запись файла через temp + os.replace (POSIX).""" path.parent.mkdir(parents=True, exist_ok=True) fd, tmp_path = tempfile.mkstemp(dir=str(path.parent), prefix='.tmp-', suffix=path.suffix) try: with os.fdopen(fd, 'w', encoding='utf-8') as f: f.write(content) f.flush() os.fsync(f.fileno()) os.replace(tmp_path, path) except Exception: try: os.unlink(tmp_path) except OSError: pass raise # ============================================================================ # Аудит v21: helpers для errors.json и completed-step проверки # ============================================================================ def save_error(step_id: str, phase: str, message: str, details: dict | None = None) -> None: """Сохранить последнюю ошибку шага в _meta/errors.json (аудит v21, проблема #5). Файл переживает потерю контекста. При следующем запуске фазы run_phase.py читает его и выводит пользователю контекст предыдущей ошибки. """ ERRORS_FILE.parent.mkdir(parents=True, exist_ok=True) data = {} if ERRORS_FILE.exists(): try: data = json.loads(ERRORS_FILE.read_text(encoding='utf-8')) except Exception: data = {} data[step_id] = { 'step_id': step_id, 'phase': phase, 'message': message, 'details': details or {}, 'timestamp': datetime.now().isoformat(), 'attempts': data.get(step_id, {}).get('attempts', 0) + 1, } ERRORS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8') def load_last_error(step_id: str) -> dict | None: """Прочитать последнюю ошибку шага из _meta/errors.json (аудит v21).""" if not ERRORS_FILE.exists(): return None try: data = json.loads(ERRORS_FILE.read_text(encoding='utf-8')) return data.get(step_id) except Exception: return None def clear_error(step_id: str) -> None: """Очистить ошибку шага после успешного выполнения фазы (аудит v21).""" if not ERRORS_FILE.exists(): return try: data = json.loads(ERRORS_FILE.read_text(encoding='utf-8')) if step_id in data: del data[step_id] ERRORS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8') except Exception: pass def get_completed_steps() -> list[str]: """Прочитать STEP_STATE.md, вернуть список ID завершённых шагов (аудит v21). Шаг считается завершённым, если он присутствует в таблице COMPLETED_STEPS. """ step_state = BASE / 'STEP_STATE.md' if not step_state.exists(): return [] content = step_state.read_text(encoding='utf-8') # Найти секцию COMPLETED_STEPS m = re.search(r'## COMPLETED_STEPS\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL) if not m: return [] section = m.group(1) # Извлечь первый столбец каждой строки таблицы (ID шага) steps = [] for line in section.split('\n'): line = line.strip() if not line.startswith('|'): continue # Пропустить заголовок и разделитель if 'Шаг' in line and 'Название' in line: continue if re.match(r'^\|[\s\-:|]+\|$', line): continue cols = [c.strip() for c in line.split('|')] # cols[0] пустой (до первой |), cols[1] = ID if len(cols) >= 2 and cols[1]: steps.append(cols[1]) return steps def find_next_pending_step(after_step_id: str) -> tuple[str, str] | None: """Найти следующий незавершённый шаг после данного (аудит v21). Возвращает (step_id, title) или None, если все шаги завершены. Используется для подсказки пользователю, что делать дальше. """ completed = set(get_completed_steps()) # Собрать все шаги в порядке их следования в YAML all_steps: list[tuple[str, str]] = [] for yfile in sorted(PLANS_DIR.glob('*.yaml')): if yfile.name.startswith('99'): continue try: data = yaml.safe_load(yfile.read_text(encoding='utf-8')) except Exception: continue for step in data.get('steps', []): sid = step.get('id', '') title = step.get('title', '') if sid: all_steps.append((sid, title)) # Найти after_step_id и вернуть следующий незавершённый try: idx = next(i for i, (s, _) in enumerate(all_steps) if s == after_step_id) except StopIteration: return None for sid, title in all_steps[idx + 1:]: if sid not in completed: return sid, title return None def check_step_already_completed(step_id: str, phase: str, force: bool) -> int: """Аудит v21: проверить, завершён ли уже шаг. Если шаг уже в COMPLETED_STEPS — вывести подсказку и выйти. Если --force — пропустить проверку (пользователь явно хочет переписать шаг). Returns: 0 — можно продолжать (шаг не завершён ИЛИ force=True) 8 — шаг уже завершён, выход с подсказкой """ if force: print(f'\n⚠️ --force: принудительный перезапуск шага {step_id} (фаза {phase})') print(' Шаг будет переписан. Это разрушительная операция.\n') return 0 completed = get_completed_steps() if step_id not in completed: return 0 # Шаг уже завершён — вывести подсказку print(f'\n{"=" * 70}') print(f'ℹ️ ШАГ {step_id} УЖЕ ЗАВЕРШЁН') print(f'{"=" * 70}') print(f'\nШаг {step_id} присутствует в STEP_STATE.md → COMPLETED_STEPS.') print('Это означает, что он был финализирован ранее (squash-merge в main).') print('\nВозможные причины, почему вы пытаетесь запустить его снова:') print(' 1. Вы случайно перепутали номер шага') print(' 2. Вы хотите переписать шаг принудительно') print(' 3. Шаг был завершён некорректно и нужно его переделать') # Подсказать следующий шаг nxt = find_next_pending_step(step_id) if nxt: next_id, next_title = nxt print(f'\n👉 СЛЕДУЮЩИЙ НЕЗАВЕРШЁННЫЙ ШАГ: {next_id} — {next_title}') print(f' Команда: uv run python3 scripts/run_phase.py {next_id} PREFLIGHT') else: print(f'\n✅ Все шаги после {step_id} уже завершены.') print(' Возможно, проект полностью выполнен.') print(f'\nЕСЛИ ВЫ ДЕЙСТВИТЕЛЬНО ХОТИТЕ ПЕРЕЗАПУСАТЬ ШАГ {step_id}:') print(f' uv run python3 scripts/run_phase.py {step_id} {phase} --force') print('\n⚠️ --force удалит progress-файл шага и запустит фазу заново.') print(' Для PREFLIGHT с --force также удаляется feature-ветка (если есть).') print(f'{"=" * 70}\n') return 8 def find_step(step_id: str) -> dict | None: """Найти шаг в YAML.""" for yfile in sorted(PLANS_DIR.glob('*.yaml')): if yfile.name.startswith('99'): continue try: data = yaml.safe_load(yfile.read_text(encoding='utf-8')) except Exception: continue for step in data.get('steps', []): if step.get('id') == step_id: return step return None def run(cmd: str, check: bool = True) -> subprocess.CompletedProcess: """Запустить команду.""" return subprocess.run(cmd, shell=True, stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=120) def run_list(args: list[str], check: bool = True, cwd: Path | None = None) -> subprocess.CompletedProcess: """FIX-1.1: Запустить команду в list form (без shell=True, безопасно от инъекций).""" return subprocess.run(args, stdout=PIPE, stderr=PIPE, text=True, cwd=cwd or BASE, timeout=120) def log(msg: str, step: str, entry_type: str = 'action', details: dict | None = None): """Авто-логирование в agent.log + errors.log для ошибок.""" print(f' [{entry_type}] {msg}') add_entry(msg, entry_type, step=step, details=details) # Дублировать ошибки в отдельный файл для отладки (аудит v21) if entry_type == 'error': errors_log = BASE / '_meta' / 'errors.log' errors_log.parent.mkdir(parents=True, exist_ok=True) from datetime import datetime ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') with open(errors_log, 'a', encoding='utf-8') as f: f.write(f'{ts} [{step}] {msg}\n') if details: for k, v in details.items(): f.write(f' {k}: {v}\n') # FIX-1.9: активировать loop detector try: # FIX-V30-3.3: использовать sys.executable вместо 'python3', чтобы # гарантированно наследовать тот же интерпретатор (uv-venv). Прямой # вызов 'python3' в среде без активного venv падал бы с ImportError на # PyYAML и других deps. AGENTS.md запрещает вызов python3 scripts/... # напрямую для агента, но внутри скриптов оркестрации это необходимо # и безопасно — мы НЕ меняли правило для пользователя. subprocess.run([sys.executable, 'scripts/orchestrate_step.py', step, '--record-action', f'{entry_type}:{msg[:80]}'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=5) except Exception: pass def set_phase(step_id: str, phase: str, status: str) -> bool: """Обновить прогресс-файл через orchestrate_step.py (FIX-1.5: проверка return code).""" # FIX-V30-3.3: sys.executable — см. комментарий в log() r = subprocess.run([sys.executable, 'scripts/orchestrate_step.py', step_id, '--set-phase', phase, status], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE) if r.returncode != 0: log(f'set_phase FAILED (exit {r.returncode}): {r.stderr[:300]}', step_id, 'error') return False return True def sed_step_state(old: str, new: str): """Атомарно обновить CURRENT_PHASE в STEP_STATE.md. BUG-FIX (v25.2): предыдущая реализация через shell sed -i была хрупкой: - не работала при Windows line endings (\\r\\n) - не сообщала об ошибке если pattern не найден - могла оставить файл в неконсистентном состоянии при краше Новая реализация через Python regex + atomic_write_text. FIX-V27-2: если pattern 'old' не найден — force-set 'new' всё равно. Это предотвращает state desync когда предыдущая фаза не установила ожидаемое значение (например, TESTING вызывается с CURRENT_PHASE=phase1 вместо testing). Раньше функция просто предупреждала и НЕ обновляла, что приводило к рассинхронизации STEP_STATE.md (машина B) и step-progress.yaml (машина A) — см. BUG-2 в задаче V27-SCRIPTS. """ step_state = BASE / 'STEP_STATE.md' if not step_state.exists(): return content = step_state.read_text(encoding='utf-8') # BUG-FIX: убрать возможные \r (Windows line endings) перед match # и нормализовать обратно после normalized = content.replace('\r\n', '\n').replace('\r', '\n') # Заменить ALL вхождения (включая возможные дубли) — только точное совпадение значения pattern = re.compile(r'(^CURRENT_PHASE:\s*)' + re.escape(old) + r'(\s*$)', re.MULTILINE) new_content, n = pattern.subn(r'\g<1>' + new + r'\g<2>', normalized) if n == 0: # FIX-V29-1: pattern не найден — проверить, не равно ли уже текущее значение new current_match = re.search(r'^CURRENT_PHASE:\s*(\S+)', normalized, re.MULTILINE) current_value = current_match.group(1) if current_match else '' if current_value == new: # Уже правильное значение — тихо пропускаем, без warning return # FIX-V27-2: pattern не найден и значение другое — force-set new force_pattern = re.compile(r'(^CURRENT_PHASE:\s*).+?(\s*$)', re.MULTILINE) new_content, n2 = force_pattern.subn(r'\g<1>' + new + r'\g<2>', normalized) if n2 > 0: if new_content != content: atomic_write_text(step_state, new_content) log(f'sed_step_state: CURRENT_PHASE {current_value} → {new} (force-set)', '', 'action') else: # CURRENT_PHASE строки вообще нет — добавить после CURRENT_STEP lines = normalized.split('\n') new_lines = [] inserted = False for line in lines: new_lines.append(line) if not inserted and line.startswith('CURRENT_STEP:'): new_lines.append(f'CURRENT_PHASE: {new}') inserted = True if not inserted: # Не нашли CURRENT_STEP — добавить в начало new_lines.insert(0, f'CURRENT_PHASE: {new}') new_content = '\n'.join(new_lines) if new_content != content: atomic_write_text(step_state, new_content) log(f'sed_step_state: CURRENT_PHASE строка не найдена — добавлена {new}', '', 'warning') return if new_content != content: atomic_write_text(step_state, new_content) log(f'sed_step_state: CURRENT_PHASE {old} → {new} ({n} замен)', '', 'action') # ============================================================================ # FIX-V24-1.6 [P0, NEW-P1-7] — Детерминированный пост-чекер для субагентов # ============================================================================ def verify_subagent(prompt_id: str, step_id: str) -> bool: """FIX-V25-3.1: вызвать детерминированный пост-чекер для субагента. Возвращает True ТОЛЬКО если скрипт существует и вернул exit 0. Возвращает False если скрипт отсутствует ИЛИ вернул non-zero. Предыдущая версия (FIX-V24-1.6) возвращала True при отсутствии скрипта (graceful degradation) — это критическая дыра: субагент формально «PASS» без какой-либо проверки. FIX-V25-3.1 закрывает её. """ verify_script = BASE / 'scripts' / 'verify_subagent.py' if not verify_script.exists(): print(f'VERIFY_SUBAGENT FAIL: verify_subagent.py not found at {verify_script}', file=sys.stderr) return False try: r = subprocess.run( # FIX-V30-3.3: sys.executable [sys.executable, 'scripts/verify_subagent.py', prompt_id, step_id], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=60 ) except subprocess.TimeoutExpired: print(f'VERIFY_SUBAGENT FAIL: timeout (60s) for {prompt_id}', file=sys.stderr) return False print(r.stdout) if r.returncode != 0: print(f'VERIFY_SUBAGENT FAIL: {prompt_id} returned exit {r.returncode}', file=sys.stderr) if r.stderr: print(r.stderr[:500], file=sys.stderr) return False return True # ============================================================================ # ФАЗА: PREFLIGHT # ============================================================================ def phase_preflight(step: dict, step_id: str, force: bool = False) -> int: """Pre-flight: создать ветку, проверить контекст.""" step_branch = step.get('branch', f'feature/step-{step_id}') step_type = step.get('step_type', '') requires_approval = step.get('requires_approval', False) print(f'\n=== PREFLIGHT для шага {step_id} ===') # Аудит v21: при --force удалить старую feature-ветку и progress-файл if force: print(f'\n--force: очистка предыдущего состояния шага {step_id}') # Удалить feature-ветку, если существует r = run(f'git rev-parse --verify {step_branch} 2>/dev/null') if r.returncode == 0: current = run('git branch --show-current').stdout.strip() if current == step_branch: run('git checkout main 2>/dev/null || true', check=False) run(f'git branch -D {step_branch} 2>/dev/null || true', check=False) log(f'--force: feature-ветка {step_branch} удалена', step_id, 'action') # Удалить progress-файл (FIX-V25-1.1: per-step файл) safe_step_id = step_id.replace('.', '-') progress_file = BASE / '_meta' / f'step-progress-{safe_step_id}.yaml' done_file = BASE / '_meta' / f'step-progress-{safe_step_id}.done.yaml' # Legacy единый файл — тоже удаляем, если остался со старых сессий legacy_progress_file = BASE / '_meta' / 'step-progress.yaml' if progress_file.exists(): progress_file.unlink() log(f'--force: {progress_file.name} удалён', step_id, 'action') if legacy_progress_file.exists(): legacy_progress_file.unlink() log('--force: _meta/step-progress.yaml (legacy) удалён', step_id, 'action') if done_file.exists(): done_file.unlink() log(f'--force: {done_file.name} удалён', step_id, 'action') # Аудит v37: ack-файл больше НЕ используется (TASK_LIST = report-only в v36+). # Оставлена очистка legacy ack-файла для миграции со старых версий. ack_file = BASE / '_meta' / f'step-{step_id}-PLAN.ack' if ack_file.exists(): ack_file.unlink() log('--force: legacy ack-файл удалён (v37 — ack-файл упразднён, TASK_LIST = report-only)', step_id, 'action') # Очистить ошибку clear_error(step_id) # 1. Проверить cwd log('Проверка cwd', step_id, 'action') r = run('test -f AGENTS.md && echo OK || echo FAIL') if 'FAIL' in r.stdout: log('ERROR: не в корне проекта (нет AGENTS.md)', step_id, 'error') return 1 log('cwd OK', step_id, 'test') # 2. Проверить git r = run('git rev-parse --is-inside-work-tree 2>/dev/null') if r.stdout.strip() != 'true': log('Git не инициализирован', step_id, 'error') return 1 # 3. Проверить STEP_STATE.md для resume step_state = BASE / 'STEP_STATE.md' if step_state.exists(): content = step_state.read_text(encoding='utf-8') # Извлечь CURRENT_PHASE — только из начала строки (YAML block) # FIX-3.1:Specification Pattern — строгое matching с валидацией значений m = re.search(r'^CURRENT_PHASE:\s*(\S+)$', content, re.MULTILINE) if m: current_phase = m.group(1) # Валидация значения (разрешены все возможные фазы шага) # FIX-3.2: STEP_STATE.md использует lowercase для CURRENT_PHASE # Допустимые значения: awaiting-continue, pre-flight, phase1, implementation, # testing, phase2, finalization, failed (все в lowercase) if current_phase not in [ 'awaiting-continue', 'pre-flight', 'phase1', 'implementation', 'testing', 'phase2', 'finalization', 'failed' ]: log(f'Invalid CURRENT_PHASE value: {current_phase}', step_id, 'error') print(f'\n⚠️ INVALID STATE: CURRENT_PHASE={current_phase}') print(' Допустимые значения: awaiting-continue, pre-flight, phase1, implementation,') print(' testing, phase2, finalization, failed') print(' Запустите: uv run python3 scripts/run_phase.py <step> PREFLIGHT --force') return 1 if current_phase == 'awaiting-continue': log('CONTINUE-ГЕЙТ: awaiting-continue. Скажите "продолжить"', step_id, 'gate') print('\n⚠️ CONTINUE-ГЕЙТ: предыдущий шаг финализирован.') print(' Скажите "продолжить" для запуска этого шага.') return 1 log(f'Resume: CURRENT_PHASE={current_phase}', step_id, 'action') # 4. Approval gate if requires_approval: approval_reason = step.get('approval_reason', '') log(f'Approval gate: {approval_reason}', step_id, 'gate') print(f'\n⏸ ТРЕБУЕТСЯ ПОДТВЕРЖДЕНИЕ: {approval_reason}') print(' Скажите "подтверждаю" для продолжения.') return 1 # 5. Создать feature-ветку r = run('git branch --show-current') current_branch = r.stdout.strip() if current_branch == step_branch: log(f'Уже на ветке {step_branch}', step_id, 'action') else: if current_branch != 'main': run('git checkout main', check=False) r = run(f'git checkout -b {step_branch}', check=False) if r.returncode != 0: run(f'git checkout {step_branch}', check=False) log(f'Ветка {step_branch} активна', step_id, 'action') # FIX-1.8: реальная проверка, что мы на feature-ветке current = subprocess.run(['git', 'branch', '--show-current'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE).stdout.strip() if current != step_branch: log(f'ERROR: не удалось переключиться на {step_branch} (current={current})', step_id, 'error') return 1 # 6. STEP_STATE.md — verify или создать из template # FIX-V27-10: создавать из template для ВСЕХ шагов, не только 1.0. # Проблема: после PR merge на GitVerse, git checkout main может удалить # STEP_STATE.md (он в .gitignore / assume-unchanged). Шаг 1.1+ тоже # должен уметь пересоздавать STEP_STATE.md из template. if not step_state.exists(): template = BASE / 'STEP_STATE.template.md' if template.exists(): import shutil shutil.copy2(template, step_state) log('STEP_STATE.md создан из template (не существовал)', step_id, 'action') # Пометить как assume-unchanged, чтобы git не отслеживал изменения run('git update-index --assume-unchanged STEP_STATE.md 2>/dev/null || true', check=False) else: log('ERROR: STEP_STATE.md и STEP_STATE.template.md отсутствуют — архив повреждён', step_id, 'error') return 1 else: log('STEP_STATE.md присутствует', step_id, 'test') sed_step_state('awaiting-continue', 'pre-flight') sed_step_state('implementation', 'pre-flight') # FIX-1.10: Сохранить main_last_commit для возможного rollback main_last_commit = subprocess.run(['git', 'log', 'main', '--format=%H', '-1'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE).stdout.strip() if main_last_commit: # Записать в _meta/step-progress-<step_id>.yaml (FIX-V25-1.1: per-step) try: # FIX-V30-3.3: sys.executable — см. комментарий в log() subprocess.run([sys.executable, 'scripts/orchestrate_step.py', step_id, '--set-main-last-commit', main_last_commit], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=5) except Exception: pass log(f'main_last_commit сохранён: {main_last_commit[:8]}', step_id, 'action') # 7. Обновить прогресс (FIX-1.5: проверять return code для PREFLIGHT) if not set_phase(step_id, 'PREFLIGHT', 'completed'): log('PREFLIGHT: set_phase(completed) failed — exiting', step_id, 'error') return 1 log('PREFLIGHT завершён', step_id, 'phase') print(f'\n✓ PREFLIGHT завершён. Ветка: {step_branch}') return 0 # ============================================================================ # ФАЗА: PHASE1 — подготовка шаблона, агент заполняет # ============================================================================ def phase_phase1(step: dict, step_id: str) -> int: """Phase 1: создать шаблон docs_target для заполнения агентом.""" docs_target = step.get('docs_target', '') step_title = step.get('title', '') verify_list = step.get('verify', ['']) verify_cmd = verify_list[0] if verify_list and isinstance(verify_list[0], str) else str(verify_list[0]) if verify_list else '' expected_result = step.get('expected_result', '') print(f'\n=== PHASE1 для шага {step_id} ===') if not docs_target: log('docs_target не указан', step_id, 'error') return 1 full = BASE / docs_target full.parent.mkdir(parents=True, exist_ok=True) if full.exists(): content = full.read_text(encoding='utf-8') # Аудит v22: для bootstrap/document не проверяем Phase 2 маркер step_type = step.get('step_type', '') if step_type in ('bootstrap', 'document'): # Для bootstrap — проверяем только что файл не пустой и есть H1 if content.strip() and content.startswith('# '): log(f'{docs_target} уже заполнен ({step_type}) — обновляю', step_id, 'action') else: log(f'{docs_target} существует, но пустой — пересоздаю', step_id, 'warning') full.unlink() elif '> Phase 1 — инструкция для AI-агента' in content: log(f'{docs_target} уже в Phase 1 — обновляю', step_id, 'action') elif '> Обучающая документация' in content: log(f'{docs_target} уже в Phase 2 — пропускаю', step_id, 'warning') # Аудит v32 (Fix B): ХАРД-ГЕЙТ — если set_phase(completed) fails, # НЕ выходим с exit 0. Раньше (v29/v31) skip-ветка логировала ошибку, # но делала sys.exit(0) — формально PHASE1 = completed, но артефакта нет. # Теперь: если _validate_phase_completion падает (no Phase 1 marker) — # это BLOCKING error, exit 1. if not set_phase(step_id, 'PHASE1', 'completed'): log('PHASE1: set_phase(completed) FAILED в skip-ветке (аудит v32 Fix B)', step_id, 'error') log('docs_target в Phase 2, но Phase 1 маркер отсутствует — это нарушение 02-documentation-rules.md', step_id, 'error') log(f'Требуется: в {docs_target} должен быть маркер "> Phase 1 — инструкция для AI-агента" ДО Phase 2', step_id, 'error') log('Решение: откатить docs_target к Phase 1 (убрать "> Обучающая документация"), либо вручную добавить Phase 1 маркер и переписать Phase 2 заново', step_id, 'error') print('\n❌ PHASE1 BLOCKED (v32 Fix B): docs_target в Phase 2, но Phase 1 маркер отсутствует.') print(f' Файл: {docs_target}') print(' Нарушение: 02-documentation-rules.md — Phase 1 обязательна перед Phase 2.') print(' Решение:') print(f' 1. Откатить docs_target: git checkout HEAD -- {docs_target}') print(f' 2. Перезапустить PHASE1: make step-run STEP={step_id} PHASE=PHASE1') print(' 3. Если файл новый — удалить и пересоздать через PHASE1') sys.exit(1) clear_error(step_id) log('Фаза PHASE1 завершена (skip — файл уже в Phase 2, маркер Phase 1 был ранее)', step_id, 'phase') print('\n✓ PHASE1: пропущен (файл уже в Phase 2, Phase 1 маркер валиден)') # FIX-1.3: обойти main()-лог «Шаблон Phase 1 создан» — он был бы ложью здесь sys.exit(0) # Создать шаблон (аудит v22: упрощённый для bootstrap/document) now = datetime.now().strftime('%Y-%m-%d %H:%M') step_type = step.get('step_type', '') # Для bootstrap/document — упрощённый шаблон без Phase 2 маркера if step_type in ('bootstrap', 'document'): template = f"""# Шаг {step_id} — {step_title} > Verify-only шаг (step_type={step_type}). Проверяет, что файлы/инструменты на месте. > Аудит v22: для {step_type}-шагов НЕ требуется Phase 2 маркер и 11 секций. > См. .agent/cards/bootstrap.card.md — упрощённая карточка. ## Цель {expected_result} ## VERIFIED_CHECKLIST ### Файлы - [ ] Файлы шага проверены (см. verify) ### Тесты - [ ] `{verify_cmd}` — PASSED ### Документация - [ ] Phase 1 написана (этот файл) - [ ] Sidebar в docs/.vitepress/config.mts обновлён (если нужно) ## Верификация ```bash {verify_cmd} ``` ## История действий агента | Время | Действие | Результат | |------|---------|-----------| | {now} | Старт Phase 1: run_phase.py | начало | | {now} | Шаблон Phase 1 создан (bootstrap) | упрощённый | | {now} | Phase 1 завершена | готов к TESTING | ## Реестр файлов шага ### Созданные файлы | Файл | Описание | |------|----------| | (нет — {step_type} не создаёт файлы) | | ### Изменённые файлы | Файл | Изменение | |------|-----------| | (нет — {step_type} не меняет файлы) | | ### Удалённые файлы | Файл | Причина | |------|---------| | (нет) | | ## Реестр тестов шага | Время | Команда | Результат | |------|---------|-----------| """ full.write_text(template, encoding='utf-8') log(f'Шаблон Phase 1 создан ({step_type}, упрощённый): {docs_target}', step_id, 'action', {'lines': len(template.split('\n'))}) sed_step_state('pre-flight', 'phase1') # FIX-1.3: НЕ помечать PHASE1 completed автоматически — только in_progress if not set_phase(step_id, 'PHASE1', 'in_progress'): log('PHASE1: set_phase(in_progress) failed', step_id, 'error') print(f'\n✓ PHASE1: упрощённый шаблон ({step_type}) создан в {docs_target}') print(' Агент должен:') print(' 1. Закрыть ВСЕ пункты VERIFIED_CHECKLIST [x] (после проверки)') print(f' 2. После заполнения запустить: uv run python3 scripts/orchestrate_step.py {step_id} --set-phase PHASE1 completed') print(f' 3. Затем: uv run python3 scripts/run_phase.py {step_id} TESTING') print(f' НЕ нужно: Phase 2 маркер, 11 секций (для {step_type})') return 0 # Для implementation-шагов — полный шаблон с Phase 1 маркером template = f"""# Шаг {step_id} — {step_title} > Phase 1 — инструкция для AI-агента. После завершения шага этот файл > будет переписан в Phase 2 (обучающий формат для человека) промптом > .agent/prompts/docs-rewriter-human.md ## Цель {expected_result} ## Контекст для обязательного чтения Агент обязан перейти по каждой ссылке и прочитать файл полностью: 1. [00-manifest.md](../../00-manifest.md) — архитектура проекта 2. [.agent/rules/01-step-lifecycle.md](../../.agent/rules/01-step-lifecycle.md) — цикл жизни шага ## План 1. Проверить предусловия шага 2. Выполнить основную задачу шага 3. Запустить верификацию ## Ожидаемый результат {expected_result} ## VERIFIED_CHECKLIST ### Файлы - [ ] Файлы шага созданы ### Тесты - [ ] `{verify_cmd}` — PASSED ### Документация - [ ] Phase 1 написана (этот файл) - [ ] Sidebar в docs/.vitepress/config.mts обновлён ## Верификация ```bash {verify_cmd} ``` ## История действий агента | Время | Действие | Результат | |------|---------|-----------| | {now} | Старт Phase 1: run_phase.py | начало | | {now} | Шаблон Phase 1 создан | 9 секций | | {now} | Контекст загружен | 00-manifest.md, .agent/rules/ | | {now} | План шага сформирован | из YAML plans/ | | {now} | Phase 1 завершена | готов к IMPLEMENTATION | ## Реестр файлов шага ### Созданные файлы | Файл | Описание | |------|----------| ### Изменённые файлы | Файл | Изменение | |------|-----------| ### Удалённые файлы | Файл | Причина | |------|---------| ## Реестр тестов шага | Время | Команда | Результат | |------|---------|-----------| """ full.write_text(template, encoding='utf-8') log(f'Шаблон Phase 1 создан: {docs_target}', step_id, 'action', {'lines': len(template.split('\n'))}) # Обновить STEP_STATE sed_step_state('pre-flight', 'phase1') # FIX-1.3: НЕ помечать PHASE1 completed автоматически — только in_progress if not set_phase(step_id, 'PHASE1', 'in_progress'): log('PHASE1: set_phase(in_progress) failed', step_id, 'error') # ============================================================ # Аудит v32 (Fix D): Post-write проверка Phase 1 маркера # ============================================================ # Сразу после создания шаблона проверить, что маркер Phase 1 на месте. # Если нет — это баг в шаблоне (например, агент перезаписал файл). # Не даём агенту перейти к TESTING без валидного Phase 1 маркера. if step_type not in ('bootstrap', 'document'): try: written = full.read_text(encoding='utf-8') if '> Phase 1 — инструкция для AI-агента' not in written: log('PHASE1 (v32 Fix D): созданный шаблон НЕ содержит Phase 1 маркер!', step_id, 'error') log(f'Файл: {docs_target}', step_id, 'error') log(f'Это нарушение 02-documentation-rules.md — Phase 1 маркер обязателен для step_type={step_type}', step_id, 'error') print('\n❌ PHASE1 BLOCKED (v32 Fix D): шаблон создан, но Phase 1 маркер отсутствует.', file=sys.stderr) print(f' Файл: {docs_target}', file=sys.stderr) print(' Ожидаемый маркер: "> Phase 1 — инструкция для AI-агента"', file=sys.stderr) print(' Решение: проверьте шаблон в run_phase.py:phase_phase1() — возможно, он повреждён.', file=sys.stderr) sys.exit(1) else: log('PHASE1 (v32 Fix D): Phase 1 маркер на месте — OK', step_id, 'test') except Exception as e: log(f'PHASE1 (v32 Fix D): post-write check failed: {e}', step_id, 'warning') print(f'\n✓ PHASE1: шаблон создан в {docs_target}') print(' Агент должен заполнить секции: Цель, Контекст, План, VERIFIED_CHECKLIST') print(f' После заполнения: uv run python3 scripts/orchestrate_step.py {step_id} --set-phase PHASE1 completed') # FIX-V25-3.2b [P1, NEW-P1-7 complete] — агент запускает verify-фазу через run_phase.py print('\n=== Творческая фаза завершена. Запусти верификатор: ===') print(f'uv run python3 scripts/run_phase.py {step_id} PHASE1_VERIFY') print('Если FAIL — исправь docs_target и перезапусти.') print('Только после PASS — переход к следующей фазе.') return 0 # ============================================================================ # ФАЗА: TESTING — запустить verify, записать результаты # ============================================================================ def phase_testing(step: dict, step_id: str) -> int: """Testing: запустить verify-команду, записать в docs_target, закрыть VERIFIED_CHECKLIST.""" # FIX-V27-3a: установить CURRENT_PHASE: testing в начале фазы # Раньше этот переход пропускался, что приводило к state desync: # TESTING вызывался с CURRENT_PHASE=phase1 (или implementation), # а sed_step_state('testing', 'phase2') в конце FAIL-ила. sed_step_state('phase1', 'testing') # если был phase1 sed_step_state('implementation', 'testing') # если был implementation verify_list = step.get('verify', ['']) verify_cmd = verify_list[0] if verify_list and isinstance(verify_list[0], str) else str(verify_list[0]) if verify_list else '' docs_target = step.get('docs_target', '') print(f'\n=== TESTING для шага {step_id} ===') if not verify_cmd: log('verify не указан — пропускаю', step_id, 'warning') # FIX-1.5: проверять return value (TESTING — не критично, не exit 1) if not set_phase(step_id, 'TESTING', 'completed'): log('TESTING: set_phase(completed) failed (verify не задан)', step_id, 'error') return 0 # Запустить verify log(f'Запуск verify: {verify_cmd}', step_id, 'test') r = run(verify_cmd, check=False) now = datetime.now().strftime('%Y-%m-%d %H:%M') if r.returncode == 0: result_str = 'PASS' log('verify PASS', step_id, 'test') else: result_str = f'FAIL (exit {r.returncode})' log(f'verify FAIL: {r.stderr[:200]}', step_id, 'error') # Записать в docs_target if docs_target and (BASE / docs_target).exists(): content = (BASE / docs_target).read_text(encoding='utf-8') test_row = f'| {now} | `{verify_cmd}` | {result_str} |\n' # Добавить строку в Реестр тестов content = re.sub( r'(\| Время \| Команда \| Результат \|\n\|------\|---------\|-----------\|\n)', rf'\1{test_row}', content, count=1 ) # Добавить запись в Историю действий (FIX-1.7: гибкий разделитель таблицы) history_row = f'| {now} | Запуск verify | {result_str} |\n' content = re.sub( r'(\| Время \| Действие \| Результат \|\n\|[\s\-|]+\|[\s\-|]+\|[\s\-|]+\|\n)', rf'\1{history_row}', content, count=1 ) # FIX-1.6: закрыть ТОЛЬКО пункт, явно соответствующий verify-команде (не blanket-regex) if r.returncode == 0: verify_pattern = re.compile( r'^(- \[ \] )(`?' + re.escape(verify_cmd) + r'`?)(.*)$', re.MULTILINE ) new_content, n = verify_pattern.subn(r'- [x] \2\3', content) if n > 0: content = new_content log(f'VERIFIED_CHECKLIST: закрыт пункт для verify-команды ({n} совпадение)', step_id, 'test') else: log('VERIFIED_CHECKLIST: пункт для verify-команды не найден — агент закроет вручную', step_id, 'warning') (BASE / docs_target).write_text(content, encoding='utf-8') log(f'Результат записан в {docs_target}', step_id, 'action') # FIX-V25.1-2 [P1, NEW-V25-3] — verify_subagent ДО set_phase(completed). # Семантически консистентно с PHASE1_VERIFY / IMPLEMENTATION_VERIFY / TESTING_VERIFY / # PHASE2A_VERIFY (см. main(): verify_subagent вызывается ДО set_phase(completed)). # Если verify_subagent FAIL — НЕ отмечать TESTING completed, exit 1. # 1. verify_cmd FAIL → mark failed, return 1 (verify_subagent не нужен — # тесты уже упали, Реестр тестов будет содержать FAIL-запись). if r.returncode != 0: sed_step_state('testing', 'phase2') if not set_phase(step_id, 'TESTING', 'failed'): log('TESTING: set_phase(failed) failed', step_id, 'error') print(f'\n✗ TESTING FAIL: verify вернул exit {r.returncode}') print(f' Команда: {verify_cmd}') print(f' stderr: {r.stderr[:300]}') return 1 # 2. verify_cmd PASS → запустить verify_subagent ДО set_phase(completed). # FIX-V25-3.3 [P1, NEW-V24-2] — детерминированный пост-чекер test-runner. if not verify_subagent('test-runner', step_id): log('verify_subagent test-runner FAIL — проверь docs_target → Реестр тестов', step_id, 'error') # FIX-V25.1-2: НЕ отмечать completed. Отметить failed и exit 1 — # тесты запущены, но Реестр тестов / VERIFIED_CHECKLIST некорректен. if not set_phase(step_id, 'TESTING', 'failed'): log('TESTING: set_phase(failed) failed', step_id, 'error') print('\n❌ TESTING verify_subagent FAIL. Проверь секцию "Реестр тестов" в docs_target.', file=sys.stderr) print(' Исправь docs_target и перезапусти:', file=sys.stderr) print(f' uv run python3 scripts/run_phase.py {step_id} TESTING_VERIFY', file=sys.stderr) return 1 # 3. verify_subagent PASS → отметить TESTING completed. sed_step_state('testing', 'phase2') if not set_phase(step_id, 'TESTING', 'completed'): log('TESTING: set_phase(completed) failed', step_id, 'error') print('\n✓ TESTING: verify PASS') # FIX-V25-3.2b [P1, NEW-P1-7 complete] — для TESTING фазы тоже выводим инструкцию # (тест-раннер уже отработал inline, но пользователь может перезапустить externally) print('\n=== Тестирование завершено. Запусти верификатор: ===') print(f'uv run python3 scripts/run_phase.py {step_id} TESTING_VERIFY') return 0 # ============================================================================ # ФАЗА: PHASE2B — проверить сохраняемые секции # ============================================================================ def phase_phase2b(step: dict, step_id: str) -> int: """Phase 2B: проверить, что сохраняемые секции не потеряны.""" docs_target = step.get('docs_target', '') print(f'\n=== PHASE2B для шага {step_id} ===') if not docs_target or not (BASE / docs_target).exists(): log(f'docs_target не существует: {docs_target}', step_id, 'error') return 1 content = (BASE / docs_target).read_text(encoding='utf-8') checks = [] # 1. История действий ≥ 5 (или ≥ 3 для verify-only шагов) # FIX-V24-3.4: bootstrap — упрощённая проверка (минимум 3 записи), # консистентно с finalize_step.py и verify_step_completion.py. step_type = step.get('step_type', '') min_history = 3 if step_type in ('document', 'user_action', 'bootstrap') else 5 match = re.search(r'## История действий агента\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL) if match: rows = re.findall(r'^\|.*\|$', match.group(1), re.MULTILINE) data_rows = [r for r in rows if not re.match(r'^\|[\s\-:|]+\|$', r) and 'Время' not in r] ok = len(data_rows) >= min_history checks.append((f'История ≥ {min_history}', ok, f'{len(data_rows)} записей')) else: checks.append(('История действий', False, 'секция не найдена')) # 2. Реестр файлов — 3 подсекции for sub in ['### Созданные файлы', '### Изменённые файлы', '### Удалённые файлы']: ok = sub in content checks.append((sub, ok, 'OK' if ok else 'отсутствует')) # 3. Реестр тестов — есть PASS match = re.search(r'## Реестр тестов шага\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL) if match: has_pass = 'PASS' in match.group(1) checks.append(('Реестр тестов has PASS', has_pass, 'OK' if has_pass else 'нет PASS')) else: checks.append(('Реестр тестов', False, 'секция не найдена')) # 4. VERIFIED_CHECKLIST — все [x] match = re.search(r'## VERIFIED_CHECKLIST\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL) if match: open_items = re.findall(r'^- \[ \]\s+(.+)$', match.group(1), re.MULTILINE) ok = len(open_items) == 0 checks.append(('VERIFIED_CHECKLIST закрыт', ok, f'{len(open_items)} незакрытых' if open_items else 'все [x]')) else: checks.append(('VERIFIED_CHECKLIST', False, 'секция не найдена')) # Вывод (аудит v21 — фикс: all_ok теперь корректно обновляется) all_ok = True for name, ok, detail in checks: icon = '✓' if ok else '✗' print(f' [{icon}] {name}: {detail}') log(f'Check "{name}": {"PASS" if ok else "FAIL"} — {detail}', step_id, 'test' if ok else 'error') if not ok: all_ok = False if all_ok: # FIX-1.5: проверять return value (PHASE2B — не критично, не exit 1) if not set_phase(step_id, 'PHASE2B', 'completed'): log('PHASE2B: set_phase(completed) failed', step_id, 'error') print('\n✓ PHASE2B: все сохраняемые секции intact') return 0 else: if not set_phase(step_id, 'PHASE2B', 'failed'): log('PHASE2B: set_phase(failed) failed', step_id, 'error') print('\n✗ PHASE2B: есть проблемы — исправьте и повторите') # Аудит v21: сохранить ошибку в errors.json save_error(step_id, 'PHASE2B', 'Сохраняемые секции повреждены', {'failed_checks': [n for n, ok, _ in checks if not ok]}) return 1 # ============================================================================ # ФАЗА: FINALIZATION — вызвать finalize_step.py # ============================================================================ def phase_finalization(step: dict, step_id: str) -> int: """Finalization: вызвать finalize_step.py + verify_subagent (FIX-V24-1.6).""" # FIX-V27-3b: установить CURRENT_PHASE: finalization в начале фазы # Раньше фаза не делала sed_step_state, и STEP_STATE.md оставался в phase2, # тогда как progress.yaml уже показывал FINALIZATION → check_state_sync # WARN + set_phase блокировался на total_attempts>=5. sed_step_state('phase2', 'finalization') sed_step_state('testing', 'finalization') # fallback если phase2 не был установлен print(f'\n=== FINALIZATION для шага {step_id} ===') log('Запуск finalize_step.py', step_id, 'action') # FIX-V30-3.3: sys.executable r = run(f'{sys.executable} scripts/finalize_step.py {step_id}', check=False) if r.returncode != 0: log(f'FINALIZATION FAIL (exit {r.returncode})', step_id, 'error') print(r.stdout) print(r.stderr, file=sys.stderr) save_error(step_id, 'FINALIZATION', f'finalize_step.py exit {r.returncode}') return 1 # FIX-V25-3.4 [P1, NEW-P1-12] — post-finalization verify_subagent FAIL — не блокировать, # но создать recovery-инструкцию. Шаг уже физически финализирован (git commit, push, # STEP_STATE обновлён, ветка удалена). Возврат return 1 не откатывает изменения, # recovery path тупиковый — поэтому логируем WARN и продолжаем. # Предыдущая версия (FIX-V24-1.6) делала return 1 — это блокировало следующий шаг, # но не откатывало git-изменения, оставляя систему в рассинхронизированном состоянии. if not verify_subagent('step-finalizer', step_id): log('post-finalization verify_subagent FAIL — шаг финализирован, но проверки не прошли', step_id, 'warning') print('\n⚠️ POST-FINALIZATION WARNING:', file=sys.stderr) print(f'Шаг {step_id} физически финализирован (git commit, push, STEP_STATE обновлён).', file=sys.stderr) print('Но verify_subagent обнаружил проблемы. Recovery:', file=sys.stderr) print(' 1. Проверьте _meta/agent.log на детали', file=sys.stderr) print(f' 2. Если критично — запустите step-rollback.md для шага {step_id}', file=sys.stderr) print(' 3. Или создайте новый шаг (1.Xa) для исправления', file=sys.stderr) # Сохранить warning в errors.json (не error) try: errors_file = BASE / '_meta' / 'errors.json' if errors_file.exists(): errors = json.loads(errors_file.read_text(encoding='utf-8')) else: errors = {} errors[step_id] = errors.get(step_id, {}) errors[step_id]['post_finalization_warning'] = True errors[step_id]['timestamp'] = datetime.now().isoformat() atomic_write_text(errors_file, json.dumps(errors, ensure_ascii=False, indent=2)) except Exception: pass # НЕ return 1 — шаг уже финализирован # FIX-V27-5 [P0]: НЕ вызывать set_phase(FINALIZATION, completed) после finalize_step.py. # Проблема: finalize_step.py переименовывает _meta/step-progress-<step_id>.yaml # в .done.yaml в конце своей работы. set_phase после этого FAIL с # "progress file does not exist" — шаг уже финализирован, но run_phase # reports FAIL. # Решение: finalize_step.py уже делает всё (git commit, STEP_STATE update, # progress rename). set_phase здесь избыточен. log('FINALIZATION завершена (finalize_step.py уже обновил progress и STEP_STATE)', step_id, 'phase') print(r.stdout) return 0 # ============================================================================ # MAIN # ============================================================================ def main(): parser = argparse.ArgumentParser(description='Универсальный исполнитель фаз шага') parser.add_argument('step_id', help='ID шага, например 1.0') parser.add_argument('phase', help='Фаза: TASK_LIST|PREFLIGHT|PHASE1|IMPLEMENTATION|TESTING|CODE_REVIEW|PHASE2A|PHASE2B|FINALIZATION ' 'или verify-фазы PHASE1_VERIFY|IMPLEMENTATION_VERIFY|TESTING_VERIFY|PHASE2A_VERIFY|CODE_REVIEW_VERIFY ' '(FIX-V25-3.2a, аудит v35 — +TASK_LIST, +CODE_REVIEW)') parser.add_argument('--force', action='store_true', help='Аудит v21: принудительно перезапустить шаг, даже если он уже в COMPLETED_STEPS') args = parser.parse_args() step = find_step(args.step_id) if step is None: print(f'ERROR: шаг {args.step_id} не найден', file=sys.stderr) sys.exit(2) phase = args.phase.upper() # ============================================================================ # Аудит v35 (требование пользователя №4): проверка соответствия ветки шагу # ============================================================================ # Перед запуском любой фазы (кроме TASK_LIST и verify-фаз) — проверить, что # текущая git-ветка соответствует шагу: feature/step-X.Y-* или feature/step-X-Y-* # Исключения: TASK_LIST (ветка ещё не создана), verify-фазы (запускаются в той же # ветке), шаг 1.0 (initial commit в main), user_action (пользователь сам делает). # # При несоответствии — exit 5 (новый код, аудит v35). # ============================================================================ phases_requiring_branch_check = ( 'PREFLIGHT', 'PHASE1', 'IMPLEMENTATION', 'TESTING', 'CODE_REVIEW', 'PHASE2A', 'PHASE2B', 'FINALIZATION', ) if phase in phases_requiring_branch_check and not args.force: # Определить step_type из YAML step_type = step.get('step_type', 'implementation') # Шаг 1.0 — коммит идёт в main (исключение) is_initial_step = (args.step_id == '1.0') if not is_initial_step and step_type != 'user_action': try: branch_result = subprocess.run( ['git', 'branch', '--show-current'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=5 ) current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else '' except Exception: current_branch = '' # Если git не инициализирован — пропустить проверку (Pre-flight сам создаст .git) if current_branch: # Парсить step_id → X, Y step_id_parts = args.step_id.split('.') if len(step_id_parts) == 2: x_part, y_part = step_id_parts # Паттерны: feature/step-X.Y-* или feature/step-X-Y-* (оба формата используются) pattern1 = re.compile(rf'^feature/step-{re.escape(x_part)}\.{re.escape(y_part)}(?:[-.]|$)') pattern2 = re.compile(rf'^feature/step-{re.escape(x_part)}-{re.escape(y_part)}(?:[-.]|$)') if current_branch == 'main': # main допустим только для PREFLIGHT (создаст ветку) или если шаг 1.0 if phase != 'PREFLIGHT': print(f'\n❌ BRANCH MISMATCH (аудит v35, требование №4)', file=sys.stderr) print(f' Ожидалась ветка: feature/step-{x_part}.{y_part}-*', file=sys.stderr) print(f' Текущая ветка: {current_branch} (main)', file=sys.stderr) print(f' Фаза: {phase}', file=sys.stderr) print('', file=sys.stderr) print(' main допустим только:', file=sys.stderr) print(' - Перед PREFLIGHT (PREFLIGHT создаст новую feature-ветку)', file=sys.stderr) print(' - После FINALIZATION (до CONTINUE-ГЕЙТ)', file=sys.stderr) print(' - Для шага 1.0 (initial commit)', file=sys.stderr) print('', file=sys.stderr) print(' Действия:', file=sys.stderr) print(f' 1. Если случайно переключились — вернитесь: git checkout feature/step-{x_part}.{y_part}-<slug>', file=sys.stderr) print(f' 2. Если шаг нужно перезапустить — Pre-flight заново (создаст новую ветку)', file=sys.stderr) sys.exit(5) elif not (pattern1.match(current_branch) or pattern2.match(current_branch)): print(f'\n❌ BRANCH MISMATCH (аудит v35, требование №4)', file=sys.stderr) print(f' Ожидалась ветка: feature/step-{x_part}.{y_part}-* (или feature/step-{x_part}-{y_part}-*)', file=sys.stderr) print(f' Текущая ветка: {current_branch}', file=sys.stderr) print(f' Фаза: {phase}', file=sys.stderr) print('', file=sys.stderr) print(' Это нарушение требования пользователя №4:', file=sys.stderr) print(' «Следить, что бы новые ветки соответствовали шагу в котором идёт разработка.»', file=sys.stderr) print('', file=sys.stderr) print(' Действия:', file=sys.stderr) print(f' 1. Если случайно переключились — вернитесь: git checkout feature/step-{x_part}.{y_part}-<slug>', file=sys.stderr) print(f' 2. Если шаг нужно перезапустить — запустите Pre-flight заново (создаст новую ветку)', file=sys.stderr) print(f' 3. Если это новая сессия и хотите продолжить существующий шаг —', file=sys.stderr) print(f' git checkout <правильная ветка> && make step-run STEP={args.step_id} PHASE={phase}', file=sys.stderr) print('', file=sys.stderr) print(f' Для принудительного запуска (ОПАСНО): --force', file=sys.stderr) sys.exit(5) else: # Ветка соответствует — логировать log(f'BRANCH CHECK OK: ветка {current_branch} соответствует шагу {args.step_id}', args.step_id, 'action') # FIX-V25-3.2a [P1, NEW-P1-7 complete] — verify-фазы для творческих PHASE1/IMPLEMENTATION/ # TESTING/PHASE2A. Ранний выход, минуя precondition/in_progress логику. # Эти фазы вызываются ПОСЛЕ творческой работы агента для детерминированной проверки # через verify_subagent.py. PASS — отметить творческую фазу completed и выйти. if phase == 'PHASE1_VERIFY': # FIX-V25-3.2a: верификация Phase 1 после заполнения агентом if not verify_subagent('docs-writer', args.step_id): print('\n❌ Phase 1 верификация FAIL. Исправь docs_target и перезапусти:', file=sys.stderr) print(f' uv run python3 scripts/run_phase.py {args.step_id} PHASE1_VERIFY', file=sys.stderr) save_error(args.step_id, 'PHASE1_VERIFY', 'verify_subagent(docs-writer) FAIL') sys.exit(1) # Если PASS — отметить PHASE1 completed if not set_phase(args.step_id, 'PHASE1', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Phase 1 верифицирована. Переход к IMPLEMENTATION.') sys.exit(0) elif phase == 'IMPLEMENTATION_VERIFY': if not verify_subagent('code-writer', args.step_id): print('\n❌ Implementation верификация FAIL. Исправь код/docs_target и перезапусти:', file=sys.stderr) print(f' uv run python3 scripts/run_phase.py {args.step_id} IMPLEMENTATION_VERIFY', file=sys.stderr) save_error(args.step_id, 'IMPLEMENTATION_VERIFY', 'verify_subagent(code-writer) FAIL') sys.exit(1) if not set_phase(args.step_id, 'IMPLEMENTATION', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Implementation верифицирован. Переход к TESTING.') sys.exit(0) elif phase == 'TESTING_VERIFY': if not verify_subagent('test-runner', args.step_id): print('\n❌ Testing верификация FAIL. Исправь Реестр тестов в docs_target и перезапусти:', file=sys.stderr) print(f' uv run python3 scripts/run_phase.py {args.step_id} TESTING_VERIFY', file=sys.stderr) save_error(args.step_id, 'TESTING_VERIFY', 'verify_subagent(test-runner) FAIL') sys.exit(1) if not set_phase(args.step_id, 'TESTING', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Testing верифицирован. Переход к PHASE2A.') sys.exit(0) elif phase == 'PHASE2A_VERIFY': if not verify_subagent('docs-rewriter-human', args.step_id): print('\n❌ Phase 2A верификация FAIL. Исправь docs_target (обучающие секции) и перезапусти:', file=sys.stderr) print(f' uv run python3 scripts/run_phase.py {args.step_id} PHASE2A_VERIFY', file=sys.stderr) save_error(args.step_id, 'PHASE2A_VERIFY', 'verify_subagent(docs-rewriter-human) FAIL') sys.exit(1) if not set_phase(args.step_id, 'PHASE2A', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Phase 2A верифицирована. Переход к PHASE2B.') sys.exit(0) elif phase == 'CODE_REVIEW_VERIFY': # Аудит v35, требование №1: верификация code-review после TESTING PASS. # Если FAIL (P0 ≥ 1 или P1 ≥ 3) — возврат на TESTING (agent чинит через debug-fixer). # Если PASS — отметить CODE_REVIEW completed, переход к PHASE2A. if not verify_subagent('code-reviewer', args.step_id): print('\n❌ Code-review верификация FAIL.', file=sys.stderr) print(' Найдены блокирующие проблемы (P0 ≥ 1 или P1 ≥ 3).', file=sys.stderr) print(' Возврат на TESTING: агент должен исправить через debug-fixer.md → code-writer.md.', file=sys.stderr) print(f' Повторить: uv run python3 scripts/run_phase.py {args.step_id} TESTING', file=sys.stderr) print(f' Затем: uv run python3 scripts/run_phase.py {args.step_id} CODE_REVIEW_VERIFY', file=sys.stderr) save_error(args.step_id, 'CODE_REVIEW_VERIFY', 'verify_subagent(code-reviewer) FAIL — P0/P1 blocking issues') # Атомарно вернуть CURRENT_PHASE на testing (через sed) sed_step_state('code-review', 'testing') sys.exit(1) if not set_phase(args.step_id, 'CODE_REVIEW', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Code-review верифицирован (P0=0, P1≤2). Переход к PHASE2A.') sys.exit(0) elif phase == 'FINAL_TESTING_VERIFY': # Аудит v36: верификация FINAL_TESTING — повторный verify после PHASE2B. # Если FAIL — возврат на PHASE2A (debug-fixer → docs-rewriter-human). # Если PASS — отметить FINAL_TESTING completed, переход к FINAL_REVIEW. if not verify_subagent('final-test-runner', args.step_id): print('\n❌ Final-testing верификация FAIL.', file=sys.stderr) print(' Повторный verify после PHASE2B провален.', file=sys.stderr) print(' Возврат на PHASE2A: debug-fixer.md → docs-rewriter-human.md.', file=sys.stderr) print(f' Повторить: uv run python3 scripts/run_phase.py {args.step_id} PHASE2A', file=sys.stderr) print(f' Затем: uv run python3 scripts/run_phase.py {args.step_id} PHASE2B', file=sys.stderr) print(f' Затем: uv run python3 scripts/run_phase.py {args.step_id} FINAL_TESTING_VERIFY', file=sys.stderr) save_error(args.step_id, 'FINAL_TESTING_VERIFY', 'verify_subagent(final-test-runner) FAIL — verify after PHASE2B failed') # Атомарно вернуть CURRENT_PHASE на phase2 (PHASE2A) sed_step_state('final-testing', 'phase2') sys.exit(1) if not set_phase(args.step_id, 'FINAL_TESTING', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Final-testing верифицирован (все verify PASS после PHASE2B). Переход к FINAL_REVIEW.') sys.exit(0) elif phase == 'FINAL_REVIEW_VERIFY': # Аудит v36: верификация FINAL_REVIEW — release-level review перед FINALIZATION. # Если FAIL (P0 ≥ 1 или P1 ≥ 3) — возврат на PHASE2A (или TESTING если проблема в коде). # Если PASS — отметить FINAL_REVIEW completed, переход к FINALIZATION. if not verify_subagent('final-reviewer', args.step_id): print('\n❌ Final-review верификация FAIL.', file=sys.stderr) print(' Release-level ревью провалено (P0 ≥ 1 или P1 ≥ 3).', file=sys.stderr) print(' Возврат: debug-fixer.md → docs-rewriter-human.md (или code-writer.md если проблема в коде).', file=sys.stderr) print(f' Повторить: uv run python3 scripts/run_phase.py {args.step_id} PHASE2A', file=sys.stderr) print(f' Затем: uv run python3 scripts/run_phase.py {args.step_id} FINAL_TESTING_VERIFY', file=sys.stderr) print(f' Затем: uv run python3 scripts/run_phase.py {args.step_id} FINAL_REVIEW_VERIFY', file=sys.stderr) save_error(args.step_id, 'FINAL_REVIEW_VERIFY', 'verify_subagent(final-reviewer) FAIL — release-level review blocking issues') # Атомарно вернуть CURRENT_PHASE на phase2 (PHASE2A) — по умолчанию # (если проблема в коде — agent может переключить на testing через sed) sed_step_state('final-review', 'phase2') sys.exit(1) if not set_phase(args.step_id, 'FINAL_REVIEW', 'completed'): sys.exit(1) clear_error(args.step_id) print('\n✓ Final-review верифицирован (P0=0, P1≤2, release readiness OK). Переход к FINALIZATION.') sys.exit(0) elif phase == 'FINAL_TESTING': # Аудит v36: фаза FINAL_TESTING — повторный verify после PHASE2B. # Скрипт выводит инструкцию для агента, который вызывает final-test-runner.md. print(f'\n=== FINAL_TESTING (аудит v36) для шага {args.step_id} ===\n') print('Главный агент должен вызвать субагента:') print(' .agent/prompts/final-test-runner.md') print('') print(f'Параметры из YAML (step {args.step_id}):') print(f' step_id: {args.step_id}') print(f' docs_target: {step.get("docs_target", "?")}') print(f' verify: {step.get("verify", "?")}') print(f' step_type: {step.get("step_type", "?")}') print('') print('После повторного verify — запустить верификацию:') print(f' make step-verify STEP={args.step_id} PROMPT=final-test-runner') print(f' # или: uv run python3 scripts/run_phase.py {args.step_id} FINAL_TESTING_VERIFY') print('') # Обновить CURRENT_PHASE на final-testing try: step_state = (BASE / 'STEP_STATE.md').read_text(encoding='utf-8') if 'CURRENT_PHASE: final-testing' not in step_state: new_state = re.sub( r'^CURRENT_PHASE:.*$', 'CURRENT_PHASE: final-testing', step_state, count=1, flags=re.MULTILINE, ) atomic_write_text(BASE / 'STEP_STATE.md', new_state) except Exception as e: log(f'FINAL_TESTING: не удалось обновить CURRENT_PHASE: {e}', args.step_id, 'warning') log(f'FINAL_TESTING: инструкции выведены, ожидается вызов final-test-runner.md', args.step_id, 'phase') sys.exit(0) elif phase == 'FINAL_REVIEW': # Аудит v36: фаза FINAL_REVIEW — release-level review перед FINALIZATION. # Скрипт выводит инструкцию для агента, который вызывает final-reviewer.md. print(f'\n=== FINAL_REVIEW (аудит v36, release-level) для шага {args.step_id} ===\n') print('Главный агент должен вызвать субагента:') print(' .agent/prompts/final-reviewer.md') print('') print(f'Параметры из YAML (step {args.step_id}):') print(f' step_id: {args.step_id}') print(f' docs_target: {step.get("docs_target", "?")}') print(f' verify: {step.get("verify", "?")}') print(f' step_type: {step.get("step_type", "?")}') print(f' context_to_read: {step.get("context_to_read", [])}') print('') print('После ревью — запустить верификацию:') print(f' make step-verify STEP={args.step_id} PROMPT=final-reviewer') print(f' # или: uv run python3 scripts/run_phase.py {args.step_id} FINAL_REVIEW_VERIFY') print('') print('Если PASS (P0=0, P1≤2) → переход к FINALIZATION (PR GATE через permission-policy.yaml).') print('Если FAIL — возврат на PHASE2A (проблема в доке) или TESTING (проблема в коде).') print('') # Обновить CURRENT_PHASE на final-review try: step_state = (BASE / 'STEP_STATE.md').read_text(encoding='utf-8') if 'CURRENT_PHASE: final-review' not in step_state: new_state = re.sub( r'^CURRENT_PHASE:.*$', 'CURRENT_PHASE: final-review', step_state, count=1, flags=re.MULTILINE, ) atomic_write_text(BASE / 'STEP_STATE.md', new_state) except Exception as e: log(f'FINAL_REVIEW: не удалось обновить CURRENT_PHASE: {e}', args.step_id, 'warning') log(f'FINAL_REVIEW: инструкции выведены, ожидается вызов final-reviewer.md', args.step_id, 'phase') sys.exit(0) elif phase == 'TASK_LIST': # Аудит v35, требование №1: фаза TASK_LIST — генерация списка задач шага. # Скрипт НЕ выполняет всю работу — он только выводит инструкциицию для агента, # который затем вызывает task-list-generator.md как субагента. # Здесь: проверить, что docs_target не существует (или существует, но без Task List секции), # вывести инструкциию агенту, обновить CURRENT_PHASE: task-list. print(f'\n=== TASK_LIST (аудит v35, требование №1) для шага {args.step_id} ===\n') print('Главный агент должен вызвать субагента:') print(' .agent/prompts/task-list-generator.md') print('') print(f'Параметры из YAML (step {args.step_id}):') print(f' step_id: {args.step_id}') print(f' step_title: {step.get("title", "?")}') print(f' step_type: {step.get("step_type", "?")}') print(f' branch: {step.get("branch", "?")}') print(f' docs_target: {step.get("docs_target", "?")}') print(f' verify: {step.get("verify", "?")}') print(f' expected_result: {step.get("expected_result", "?")[:200]}...') print('') # Аудит v36: TASK_LIST = report-only, НЕ ждёт «выполняй» print('Аудит v36: TASK_LIST = report-only. Выполнение продолжается автоматически.') print('Ack-файл больше НЕ требуется.') print('Затем:') print(f' make step-init STEP={args.step_id} # v36: --no-ack по умолчанию') print(f' make step-run STEP={args.step_id} PHASE=PREFLIGHT') print('') # Обновить CURRENT_PHASE sed_step_state('pre-flight', 'task-list') # если было pre-flight (например, после STAGE-ANALYSIS), поставить task-list # Или просто поставить task-list (если текущая фаза — что-то другое) try: step_state = (BASE / 'STEP_STATE.md').read_text(encoding='utf-8') if 'CURRENT_PHASE: task-list' not in step_state: # Атомарная замена любой CURRENT_PHASE строки на task-list new_state = re.sub( r'^CURRENT_PHASE:.*$', 'CURRENT_PHASE: task-list', step_state, count=1, flags=re.MULTILINE, ) atomic_write_text(BASE / 'STEP_STATE.md', new_state) except Exception as e: log(f'TASK_LIST: не удалось обновить CURRENT_PHASE: {e}', args.step_id, 'warning') log(f'TASK_LIST: инструкции выведены, ожидается вызов task-list-generator.md', args.step_id, 'phase') sys.exit(0) # Аудит v37 (FIX критического бага v36.1): ack-файл больше НЕ требуется. # В v36/v36.1 оставался legacy-код, требовавший ack-файл при отсутствии progress-файла. # Это прямой конфликт с v36 (TASK_LIST = report-only, ack-файл упразднён). # Теперь: если progress-файла нет — auto-init через orchestrate_step.py (без ack-файла). # FIX-V25-1.1: per-step progress file progress_file = BASE / '_meta' / f"step-progress-{args.step_id.replace('.', '-')}.yaml" if not progress_file.exists(): # Аудит v37: auto-init без ack-файла (TASK_LIST = report-only в v36+) print(f'INFO (v37): progress file missing for step {args.step_id}, auto-initializing...', file=sys.stderr) print(f' → Аудит v36: ack-файл больше НЕ требуется (TASK_LIST = report-only)', file=sys.stderr) print(f' → Запуск: scripts/orchestrate_step.py {args.step_id} --init-progress', file=sys.stderr) # Auto-init progress-файла без ack-файла (v37 — по умолчанию) # FIX-V30-3.3: sys.executable — см. комментарий в log() r = subprocess.run([sys.executable, 'scripts/orchestrate_step.py', args.step_id, '--init-progress', '--no-ack'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE) if r.returncode != 0: print('ERROR: failed to init progress file:', file=sys.stderr) print(r.stderr, file=sys.stderr) print('', file=sys.stderr) print('Manual recovery:', file=sys.stderr) print(f' uv run python3 scripts/orchestrate_step.py {args.step_id} --init-progress --no-ack', file=sys.stderr) sys.exit(3) # FIX-V27-7 [P0]: убран ошибочный elif — он срабатывал когда progress-файл # СУЩЕСТВУЕТ и фаза НЕ PREFLIGHT, и выдавал "progress file missing" для # существующего файла. Это блокировало все фазы кроме PREFLIGHT. # Аудит v21: проверить, завершён ли уже шаг (с подсказкой следующего) rc = check_step_already_completed(args.step_id, phase, args.force) if rc != 0: sys.exit(rc) # Аудит v21: показать контекст предыдущей ошибки, если она есть last_err = load_last_error(args.step_id) if last_err: print(f'\n⚠️ ПРЕДЫДУЩАЯ ОШИБКА ШАГА {args.step_id} (попытка #{last_err.get("attempts", 1)}):') print(f' Фаза: {last_err.get("phase", "?")}') print(f' Сообщение: {last_err.get("message", "?")}') details = last_err.get('details', {}) if details: print(f' Детали: {json.dumps(details, ensure_ascii=False)[:300]}') print(f' Время: {last_err.get("timestamp", "?")[:19]}') print() # Авто-лог: запуск фазы log(f'Фаза {phase} начата для шага {args.step_id}', args.step_id, 'phase') # FIX-V24-3.1: can_start_phase ДО set_phase(in_progress). # Если can_start_phase FAIL — exit 3 БЕЗ вызова set_phase, # иначе шаг останется in_progress без возможности перезапуска. # FIX-V30-3.3: sys.executable — см. комментарий в log() r = subprocess.run([sys.executable, 'scripts/orchestrate_step.py', args.step_id, '--can-start', phase], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE) if r.returncode != 0: print('PRECONDITION FAILED:', file=sys.stderr) print(r.stderr, file=sys.stderr) sys.exit(3) # Обновить прогресс: in_progress (FIX-1.5: проверять return value) # Только после успешной проверки предусловий (FIX-V24-3.1) # ============================================================ # Аудит v32 (Fix C): Loop detector threshold check ПЕРЕД запуском фазы # ============================================================ # Раньше loop_detector считал total_attempts в progress-файле, но run_phase.py # не проверял threshold перед запуском. Агент мог повторять фазу 5+ раз, # и loop_detector молчал. Теперь: если total_attempts >= 5 — BLOCK с exit 4. try: progress_data = subprocess.run( [sys.executable, 'scripts/orchestrate_step.py', args.step_id, '--show-progress'], stdout=PIPE, stderr=PIPE, text=True, cwd=BASE, timeout=10 ).stdout # Найти total_attempts для текущей фазы # Формат в progress-файле: phases: PHASE1: total_attempts: N # Но --show-progress выводит текст — парсим import re as _re # Прямое чтение progress-файла (надёжнее) safe_step = args.step_id.replace('.', '-') prog_file = BASE / '_meta' / f'step-progress-{safe_step}.yaml' if prog_file.exists(): prog_text = prog_file.read_text(encoding='utf-8') # Найти блок текущей фазы phase_block_pattern = _re.compile( rf'^\s+{phase}:\s*\n((?:\s+[a-z_]+:.*\n)*)', _re.MULTILINE ) m = phase_block_pattern.search(prog_text) if m: block = m.group(1) ta_match = _re.search(r'total_attempts:\s*(\d+)', block) if ta_match: total = int(ta_match.group(1)) if total >= 5: log(f'LOOP DETECTOR (v32 Fix C): фаза {phase} достигла total_attempts={total} (cross-session limit)', args.step_id, 'error') print(f'\n❌ LOOP DETECTOR: фаза {phase} уже запускалась {total} раз (cross-session).', file=sys.stderr) print(' Это признак зацикливания — агент повторяет одно и то же действие без прогресса.', file=sys.stderr) print(' Решение:', file=sys.stderr) print(' 1. Проанализируйте _meta/agent.log — что агент делает на каждой итерации', file=sys.stderr) print(f' 2. Сбросить loop detector: make loop-reset STEP={args.step_id}', file=sys.stderr) print(f' 3. Если проблема системная — откатить шаг: make step-reset STEP={args.step_id}', file=sys.stderr) print(' 4. Эскалировать к пользователю — возможно, шаг требует правки YAML', file=sys.stderr) sys.exit(4) elif total >= 3: log(f'LOOP DETECTOR WARNING: фаза {phase} total_attempts={total} (3 из 5 — близко к пределу)', args.step_id, 'warning') print(f'⚠️ LOOP DETECTOR: фаза {phase} запускалась {total} раз. Осталось {5-total} попыток до блокировки.', file=sys.stderr) except Exception as e: log(f'loop detector check failed (non-blocking): {e}', args.step_id, 'warning') if not set_phase(args.step_id, phase, 'in_progress'): log(f'set_phase(in_progress) failed для фазы {phase}', args.step_id, 'error') # PREFLIGHT и FINALIZATION — критичные фазы, exit 1 при ошибке if phase in ('PREFLIGHT', 'FINALIZATION'): save_error(args.step_id, phase, f'set_phase(in_progress) failed for {phase}') sys.exit(1) # Маршрутизация handlers = { 'PREFLIGHT': phase_preflight, 'PHASE1': phase_phase1, 'TESTING': phase_testing, 'PHASE2B': phase_phase2b, 'FINALIZATION': phase_finalization, } handler = handlers.get(phase) if handler: if phase == 'PREFLIGHT': exit_code = handler(step, args.step_id, args.force) else: exit_code = handler(step, args.step_id) else: # IMPLEMENTATION и PHASE2A — агент делает сам, но скрипт помогает print(f'\n=== {phase} для шага {args.step_id} ===') if phase == 'IMPLEMENTATION': # FIX-V27-3d: установить CURRENT_PHASE: implementation в начале фазы # Раньше PHASE1 → IMPLEMENTATION не делал sed_step_state, и STEP_STATE.md # оставался в phase1, тогда как progress.yaml показывал IMPLEMENTATION. sed_step_state('phase1', 'implementation') sed_step_state('testing', 'implementation') # fallback если фаза перезапускается print('Эта фаза требует творческой работы агента.') print('Скрипт подготовил окружение. Агент, выполни задачу:') print(f' 1. Прочитай docs_target: {step.get("docs_target", "")}') print(' 2. Создай файлы согласно плану') print(' 3. Обнови Историю действий и Реестр файлов в docs_target') print(f' 4. После завершения — запусти: run_phase.py {args.step_id} TESTING') # FIX-V25-3.2b [P1, NEW-P1-7 complete] — агент запускает verify-фазу через run_phase.py print('\n=== Творческая фаза завершена. Запусти верификатор: ===') print(f'uv run python3 scripts/run_phase.py {args.step_id} IMPLEMENTATION_VERIFY') print('Если FAIL — исправь docs_target и перезапусти.') print('Только после PASS — переход к следующей фазе.') log('Фаза IMPLEMENTATION — ожидает творческой работы агента', args.step_id, 'action') exit_code = 0 elif phase == 'PHASE2A': # FIX-V27-3c: установить CURRENT_PHASE: phase2 в начале PHASE2A # Раньше PHASE2A не делал sed_step_state, и STEP_STATE.md оставался в # testing (или phase1 если testing не успел установиться), что приводило # к state desync с progress.yaml=PHASE2A. sed_step_state('testing', 'phase2') sed_step_state('phase1', 'phase2') # fallback если testing не был установлен sed_step_state('implementation', 'phase2') # fallback # Автоматически заменить Phase 1 маркер на Phase 2 (аудит v19/v20) docs_target = step.get('docs_target', '') if docs_target and (BASE / docs_target).exists(): content = (BASE / docs_target).read_text(encoding='utf-8') # Заменить ТОЛЬКО маркер (не трогать остальной файл!) old_marker = '> Phase 1 — инструкция для AI-агента' new_marker = '> Обучающая документация. После прохождения этого материала вы сможете\n> воспроизвести шаг вручную без AI-агента.' if old_marker in content: # Заменить построчно: найти 3 строки маркера и заменить # FIX-V24-3.3: skip_next_2 был багом — после первой итерации флаг # сбрасывался, и 3-я строка Phase 1 маркера оставалась как dangling blockquote. # Используем счётчик skip_remaining для пропуска ровно 2 строк. lines = content.split('\n') new_lines = [] skip_remaining = 0 for i, line in enumerate(lines): if skip_remaining > 0: skip_remaining -= 1 continue if old_marker in line: # Заменить эту строку + следующие 2 строки (маркер из 3 строк) new_lines.append(new_marker.split('\n')[0]) new_lines.append(new_marker.split('\n')[1]) # Пропустить следующие 2 строки Phase 1 маркера skip_remaining = 2 continue new_lines.append(line) content = '\n'.join(new_lines) (BASE / docs_target).write_text(content, encoding='utf-8') log(f'Phase 2 маркер установлен в {docs_target}', args.step_id, 'action') elif 'Обучающая документация' in content: log('Phase 2 маркер уже установлен', args.step_id, 'warning') else: log('НЕ найден Phase 1 маркер — агент должен установить Phase 2 вручную', args.step_id, 'warning') print('Phase 2 маркер установлен автоматически.') print('Агент, добавь обучающие секции:') print(' 1. ## Что вы получите в конце шага') print(' 2. ## Предварительные требования') print(' 3. ## Почему именно так (2+ абзаца)') print(' 4. ## Пошаговая инструкция (2+ подшага)') print(' 5. ## Как проверить') print(' 6. ## Типичные ошибки и их решения (таблица 2+ строк)') print(' 7. ## Что дальше') print(' НЕ трогай: История, Реестры, VERIFIED_CHECKLIST') print(f' После завершения — запусти: run_phase.py {args.step_id} PHASE2B') # FIX-V25-3.2b [P1, NEW-P1-7 complete] — агент запускает verify-фазу через run_phase.py print('\n=== Творческая фаза завершена. Запусти верификатор: ===') print(f'uv run python3 scripts/run_phase.py {args.step_id} PHASE2A_VERIFY') print('Если FAIL — исправь docs_target и перезапусти.') print('Только после PASS — переход к следующей фазе.') log('Фаза PHASE2A — маркер установлен, агент заполняет секции', args.step_id, 'action') exit_code = 0 else: print(f'Неизвестная фаза: {phase}') exit_code = 1 # Аудит v21: cleanup errors.json при успехе + save_error при провале # FIX-1.3: для PHASE1/IMPLEMENTATION/PHASE2A фаза только in_progress — не логировать «завершена успешно» if exit_code == 0: clear_error(args.step_id) if phase == 'PHASE1': log(f'Шаблон Phase 1 создан. Агент должен заполнить секции. После заполнения агент запускает: uv run python3 scripts/orchestrate_step.py {args.step_id} --set-phase PHASE1 completed', args.step_id, 'phase') elif phase in ('IMPLEMENTATION', 'PHASE2A'): log(f'Фаза {phase} in_progress. Агент должен выполнить творческую работу. После завершения: uv run python3 scripts/orchestrate_step.py {args.step_id} --set-phase {phase} completed', args.step_id, 'phase') else: log(f'Фаза {phase} завершена успешно', args.step_id, 'phase') else: log(f'Фаза {phase} завершилась с ошибкой (exit {exit_code})', args.step_id, 'error') save_error(args.step_id, phase, f'Фаза {phase} завершилась с ошибкой (exit {exit_code})') sys.exit(exit_code) if __name__ == '__main__': main()