feat: ESCALATE verdict, issue tracker, onboarding commands
Add 3-verdict system (PASS/FAIL/ESCALATE) with priority handling across simple and phased pipelines. Senior reviewers can now escalate issues requiring human intervention, immediately breaking the review loop. - ESCALATE verdict extraction with highest priority over PASS/FAIL - Issue Tracker tables (ISS-NNN) carried across iterations - Auto-escalate heuristic using (file, keyword) composite fingerprints - Report restructuring: executive view first (verdict → tracker → metrics) - Onboarding: `doctor`, `demo`, `init --guided` commands - Exit codes: PASS=0, FAIL=1, ESCALATE=2 - 87 tests passing (54 config + 25 onboarding + 8 integration) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
from cross_eval import __version__
|
||||
from cross_eval.config import REASONING_EFFORT_CHOICES
|
||||
from cross_eval.config import REASONING_EFFORT_CHOICES, resolve_agent_shorthand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,7 +38,7 @@ coders: [claude-coder]
|
||||
reviewers: [claude-reviewer]
|
||||
# seniors: [codex-senior]
|
||||
|
||||
# 파이프라인 종류: simple | cross-review | review-only | review-fix
|
||||
# 파이프라인 종류: simple | cross-review | plan-review | review-only | review-fix | coding-review-fix
|
||||
pipeline: preset:{preset}
|
||||
|
||||
# 반복 설정
|
||||
@@ -145,7 +145,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"AI 코딩 에이전트의 결과물을 자동으로 검증하는 CLI 도구.\n"
|
||||
"\n"
|
||||
"동작 방식:\n"
|
||||
" 1. 기획서(plan)를 바탕으로 Coder 에이전트가 코드를 생성\n"
|
||||
" 1. 기획서(plan)를 바탕으로 Coder 에이전트가 코드를 작성\n"
|
||||
" 2. Reviewer 에이전트가 기획서 대비 코드를 검토하고 PASS/FAIL 판정\n"
|
||||
" 3. FAIL이면 피드백을 반영해서 1~2를 반복 (최대 N회)\n"
|
||||
"\n"
|
||||
@@ -195,11 +195,19 @@ def main(argv: list[str] | None = None) -> int:
|
||||
init_parser.add_argument(
|
||||
"--preset",
|
||||
default="simple",
|
||||
choices=["simple", "cross-review", "review-only", "review-fix"],
|
||||
choices=[
|
||||
"simple",
|
||||
"cross-review",
|
||||
"plan-review",
|
||||
"review-only",
|
||||
"review-fix",
|
||||
"coding-review-fix",
|
||||
],
|
||||
help=(
|
||||
"파이프라인 종류 (기본: simple). "
|
||||
"simple=코딩+리뷰, cross-review=교차리뷰, "
|
||||
"review-only=리뷰만, review-fix=리뷰수렴+자동수정"
|
||||
"simple=코딩+리뷰, cross-review=교차리뷰, plan-review=문서기획검토, "
|
||||
"review-only=리뷰만, review-fix=리뷰수렴+자동수정, "
|
||||
"coding-review-fix=초기코딩후리뷰수렴"
|
||||
),
|
||||
)
|
||||
init_parser.add_argument(
|
||||
@@ -208,13 +216,65 @@ def main(argv: list[str] | None = None) -> int:
|
||||
choices=["en", "ko"],
|
||||
help="프롬프트 언어 (기본: ko)",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--guided",
|
||||
action="store_true",
|
||||
help="대화형 설정 마법사 실행",
|
||||
)
|
||||
|
||||
# --- doctor ---
|
||||
doctor_parser = subparsers.add_parser(
|
||||
"doctor",
|
||||
help="실행 환경 점검 (CLI 설치, 인증, 설정 파일 검증)",
|
||||
description="cross-eval 실행에 필요한 환경을 점검합니다.",
|
||||
)
|
||||
doctor_parser.add_argument(
|
||||
"--dir",
|
||||
type=Path,
|
||||
default=Path("."),
|
||||
help="점검할 디렉토리 (기본: 현재 디렉토리)",
|
||||
)
|
||||
|
||||
# --- demo ---
|
||||
demo_parser = subparsers.add_parser(
|
||||
"demo",
|
||||
help="내장 데모 실행 (파이프라인 동작 체험)",
|
||||
description=(
|
||||
"내장된 간단한 기획서로 cross-eval 파이프라인의 전체 동작을 체험합니다.\n"
|
||||
"기본값은 mock 모드(시뮬레이션)이며, --live로 실제 에이전트를 호출할 수 있습니다."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
demo_parser.add_argument(
|
||||
"--live",
|
||||
action="store_true",
|
||||
help="실제 에이전트를 호출하여 데모 실행 (API 비용 발생)",
|
||||
)
|
||||
demo_parser.add_argument(
|
||||
"--preset",
|
||||
default="simple",
|
||||
choices=["simple", "review-fix", "coding-review-fix"],
|
||||
help="데모할 파이프라인 종류 (기본: simple)",
|
||||
)
|
||||
demo_parser.add_argument(
|
||||
"--escalate",
|
||||
action="store_true",
|
||||
help="ESCALATE 시나리오 데모 (mock 모드 전용)",
|
||||
)
|
||||
demo_parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=None,
|
||||
metavar="SEC",
|
||||
help="에이전트 호출 제한 시간 (--live 전용)",
|
||||
)
|
||||
|
||||
# --- run ---
|
||||
run_parser = subparsers.add_parser(
|
||||
"run",
|
||||
help="검증 파이프라인 실행",
|
||||
description=(
|
||||
"기획서(plan)를 기반으로 AI 에이전트가 코드 생성과 리뷰를 반복합니다.\n"
|
||||
"기획서(plan)를 기반으로 AI 에이전트가 코딩과 리뷰를 반복합니다.\n"
|
||||
"\n"
|
||||
"설정 파일 없이 바로 실행할 수 있고, config.yaml로도 실행할 수 있습니다.\n"
|
||||
"CLI 옵션이 config.yaml보다 우선합니다."
|
||||
@@ -222,13 +282,19 @@ def main(argv: list[str] | None = None) -> int:
|
||||
epilog=(
|
||||
"파이프라인 종류 (--preset):\n"
|
||||
" ┌──────────────┬─────────────────────────────────────────────────────┐\n"
|
||||
" │ simple │ Coder가 코드 생성 → Reviewer가 리뷰 │\n"
|
||||
" │ (기본값) │ FAIL이면 피드백 반영해서 재생성, PASS까지 반복 │\n"
|
||||
" │ simple │ Coder가 코드 작성 → Reviewer가 리뷰 │\n"
|
||||
" │ (기본값) │ FAIL이면 피드백 반영해서 재코딩, PASS까지 반복 │\n"
|
||||
" ├──────────────┼─────────────────────────────────────────────────────┤\n"
|
||||
" │ review-fix │ 2단계 파이프라인: │\n"
|
||||
" │ │ Reviewer N명 병렬 리뷰 → 취합 → 수정 → 재검증 │\n"
|
||||
" ├──────────────┼─────────────────────────────────────────────────────┤\n"
|
||||
" │ review-only │ 코드 생성 없이 Reviewer N명이 기존 코드만 검토 │\n"
|
||||
" │ coding- │ 3단계 파이프라인: │\n"
|
||||
" │ review-fix │ 초기 코딩 1회 → 리뷰 취합 → 수정 → 재검증 반복 │\n"
|
||||
" ├──────────────┼─────────────────────────────────────────────────────┤\n"
|
||||
" │ plan-review │ 구현 전 기획서/체크리스트/문서를 검토 │\n"
|
||||
" │ │ 필요하면 현재 코드베이스와의 정합성도 점검 │\n"
|
||||
" ├──────────────┼─────────────────────────────────────────────────────┤\n"
|
||||
" │ review-only │ 코드 작성 없이 Reviewer N명이 기존 코드만 검토 │\n"
|
||||
" │ │ (이미 작성된 코드의 품질 감사용) │\n"
|
||||
" ├──────────────┼─────────────────────────────────────────────────────┤\n"
|
||||
" │ cross-review │ Coder 2명이 각각 구현 → 상대방 코드를 교차 리뷰 │\n"
|
||||
@@ -239,10 +305,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
" ┌──────────────────┬─────────┬───────────┬──────────────────────────┐\n"
|
||||
" │ 이름 │ CLI │ 기본 모델 │ 역할 │\n"
|
||||
" ├──────────────────┼─────────┼───────────┼──────────────────────────┤\n"
|
||||
" │ claude-coder │ claude │ opus │ 코드 생성 │\n"
|
||||
" │ claude-coder │ claude │ opus │ 코드 작성 │\n"
|
||||
" │ claude-reviewer │ claude │ opus │ 코드 리뷰 │\n"
|
||||
" │ claude-senior │ claude │ opus │ 리뷰 취합/판정 │\n"
|
||||
" │ codex-coder │ codex │ gpt-5.4 │ 코드 생성 │\n"
|
||||
" │ codex-coder │ codex │ gpt-5.4 │ 코드 작성 │\n"
|
||||
" │ codex-reviewer │ codex │ gpt-5.4 │ 코드 리뷰 │\n"
|
||||
" │ codex-senior │ codex │ gpt-5.4 │ 리뷰 취합/판정 │\n"
|
||||
" └──────────────────┴─────────┴───────────┴──────────────────────────┘\n"
|
||||
@@ -267,10 +333,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
" cross-eval run --plan plan.md --preset review-fix \\\n"
|
||||
" --reviewer claude --reviewer codex\n"
|
||||
"\n"
|
||||
" 초기 코딩 후 리뷰 수렴 + 자동 수정 (coding-review-fix):\n"
|
||||
" cross-eval run --plan plan.md --preset coding-review-fix \\\n"
|
||||
" --reviewer claude --reviewer codex\n"
|
||||
"\n"
|
||||
" 기존 코드 리뷰만 (review-only):\n"
|
||||
" cross-eval run --plan plan.md --preset review-only \\\n"
|
||||
" --reviewer claude --reviewer codex\n"
|
||||
"\n"
|
||||
" 구현 전 문서/기획 검토 (plan-review):\n"
|
||||
" cross-eval run --plan plan.md --preset plan-review \\\n"
|
||||
" --reviewer claude --reviewer codex\n"
|
||||
"\n"
|
||||
" 모델 변경:\n"
|
||||
" cross-eval run --plan plan.md --model sonnet\n"
|
||||
"\n"
|
||||
@@ -341,7 +415,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
help="모든 에이전트의 모델을 한번에 변경 (예: sonnet, opus)",
|
||||
)
|
||||
agent_group.add_argument(
|
||||
"--generator-model", default=None, metavar="MODEL",
|
||||
"--coder-model", default=None, metavar="MODEL",
|
||||
help="Coder 에이전트 모델만 변경",
|
||||
)
|
||||
agent_group.add_argument(
|
||||
@@ -353,7 +427,14 @@ def main(argv: list[str] | None = None) -> int:
|
||||
pipe_group = run_parser.add_argument_group("파이프라인")
|
||||
pipe_group.add_argument(
|
||||
"--preset", default=None,
|
||||
choices=["simple", "cross-review", "review-only", "review-fix"],
|
||||
choices=[
|
||||
"simple",
|
||||
"cross-review",
|
||||
"plan-review",
|
||||
"review-only",
|
||||
"review-fix",
|
||||
"coding-review-fix",
|
||||
],
|
||||
help="파이프라인 종류 (기본: simple). 각 종류 설명은 아래 참조",
|
||||
)
|
||||
pipe_group.add_argument(
|
||||
@@ -400,6 +481,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if args.command == "init":
|
||||
return cmd_init(args)
|
||||
elif args.command == "doctor":
|
||||
return cmd_doctor(args)
|
||||
elif args.command == "demo":
|
||||
return cmd_demo(args)
|
||||
elif args.command == "run":
|
||||
return cmd_run(args)
|
||||
else:
|
||||
@@ -407,9 +492,186 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_doctor(args: argparse.Namespace) -> int:
|
||||
"""Run environment health checks."""
|
||||
from cross_eval.doctor import format_doctor_results, run_doctor
|
||||
|
||||
checks = run_doctor(args.dir.resolve())
|
||||
print(format_doctor_results(checks))
|
||||
|
||||
has_critical = any(not c.passed and c.critical for c in checks)
|
||||
return 1 if has_critical else 0
|
||||
|
||||
|
||||
def cmd_demo(args: argparse.Namespace) -> int:
|
||||
"""Run a built-in demo to show the pipeline lifecycle."""
|
||||
from cross_eval.demo import run_live_demo, run_mock_demo
|
||||
|
||||
if args.live:
|
||||
print("\n⚠ --live 모드: 실제 AI 에이전트를 호출합니다 (API 비용 발생).")
|
||||
print(" 내장 피보나치 함수 기획서를 사용합니다.\n")
|
||||
try:
|
||||
answer = input("계속하시겠습니까? [y/N] ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n취소됨.")
|
||||
return 0
|
||||
if answer not in ("y", "yes"):
|
||||
print("취소됨.")
|
||||
return 0
|
||||
|
||||
try:
|
||||
raw_timeout = args.timeout if args.timeout is not None else 0
|
||||
agent_timeout = None if raw_timeout == 0 else raw_timeout
|
||||
result = run_live_demo(preset=args.preset, timeout=agent_timeout)
|
||||
print(f"\nResult: {result.final_verdict}")
|
||||
print(f"Iterations: {len(result.iterations)}")
|
||||
if result.run_dir:
|
||||
print(f"Output: {result.run_dir}/")
|
||||
return 0
|
||||
except (RuntimeError, KeyboardInterrupt) as e:
|
||||
if isinstance(e, KeyboardInterrupt):
|
||||
print("\nInterrupted.")
|
||||
return 130
|
||||
print(f"Demo error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
run_mock_demo(preset=args.preset, show_escalate=args.escalate)
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Guided init wizard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_PRESET_DESCRIPTIONS = {
|
||||
"simple": "코딩 + 리뷰 (가장 기본)",
|
||||
"review-fix": "리뷰 → 취합 → 수정 → 재검증 반복",
|
||||
"coding-review-fix": "초기 코딩 + 리뷰 수렴 반복",
|
||||
"plan-review": "구현 전 기획서/문서 검토",
|
||||
"review-only": "기존 코드만 리뷰 (코딩 없음)",
|
||||
"cross-review": "2명이 각각 구현 후 교차 리뷰",
|
||||
}
|
||||
|
||||
_PRESET_ORDER = [
|
||||
"simple", "review-fix", "coding-review-fix",
|
||||
"plan-review", "review-only", "cross-review",
|
||||
]
|
||||
|
||||
|
||||
def _prompt_choice(
|
||||
message: str,
|
||||
choices: list[str],
|
||||
descriptions: dict[str, str] | None = None,
|
||||
default: int = 1,
|
||||
) -> str:
|
||||
"""Prompt user to pick from a numbered list."""
|
||||
print(f"\n{message}")
|
||||
for i, choice in enumerate(choices, 1):
|
||||
desc = f" — {descriptions[choice]}" if descriptions and choice in descriptions else ""
|
||||
marker = " (기본)" if i == default else ""
|
||||
print(f" {i}. {choice}{desc}{marker}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
raw = input(f"선택 [{default}]: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
return choices[default - 1]
|
||||
if not raw:
|
||||
return choices[default - 1]
|
||||
try:
|
||||
idx = int(raw)
|
||||
if 1 <= idx <= len(choices):
|
||||
return choices[idx - 1]
|
||||
except ValueError:
|
||||
if raw in choices:
|
||||
return raw
|
||||
print(f" 1-{len(choices)} 사이 숫자를 입력하세요.")
|
||||
|
||||
|
||||
def _prompt_text(message: str, default: str = "") -> str:
|
||||
"""Prompt for text input with default."""
|
||||
suffix = f" [{default}]" if default else ""
|
||||
try:
|
||||
raw = input(f"{message}{suffix}: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
return default
|
||||
return raw or default
|
||||
|
||||
|
||||
def _run_guided_init(target: Path) -> dict:
|
||||
"""Interactive setup wizard. Returns settings dict."""
|
||||
print("\n━━━ cross-eval 설정 마법사 ━━━\n")
|
||||
|
||||
lang = _prompt_choice(
|
||||
"언어 / Language:",
|
||||
["ko", "en"],
|
||||
{"ko": "한국어", "en": "English"},
|
||||
default=1,
|
||||
)
|
||||
|
||||
preset = _prompt_choice(
|
||||
"파이프라인 종류:",
|
||||
_PRESET_ORDER,
|
||||
_PRESET_DESCRIPTIONS,
|
||||
default=1,
|
||||
)
|
||||
|
||||
print("\n--- 에이전트 설정 ---")
|
||||
print(" 사용 가능: claude, codex (또는 claude-coder, codex-reviewer 등)")
|
||||
|
||||
coder = _prompt_text(" Coder 에이전트", default="claude")
|
||||
reviewer = _prompt_text(" Reviewer 에이전트", default="claude")
|
||||
|
||||
needs_senior = preset in ("review-fix", "coding-review-fix")
|
||||
senior = ""
|
||||
if needs_senior:
|
||||
senior = _prompt_text(" Senior 에이전트", default=reviewer)
|
||||
else:
|
||||
senior = _prompt_text(" Senior 에이전트 (선택, Enter로 건너뛰기)", default="")
|
||||
|
||||
max_iter = _prompt_text("최대 반복 횟수", default="3")
|
||||
try:
|
||||
max_iter_int = int(max_iter)
|
||||
except ValueError:
|
||||
max_iter_int = 3
|
||||
|
||||
create_templates = _prompt_text(
|
||||
"\n템플릿 파일(plan.md, checklist.md) 생성?", default="Y",
|
||||
).lower() in ("y", "yes", "")
|
||||
|
||||
return {
|
||||
"lang": lang,
|
||||
"preset": preset,
|
||||
"coder": coder,
|
||||
"reviewer": reviewer,
|
||||
"senior": senior,
|
||||
"max_iter": max_iter_int,
|
||||
"create_templates": create_templates,
|
||||
}
|
||||
|
||||
|
||||
def cmd_init(args: argparse.Namespace) -> int:
|
||||
"""Scaffold a new cross-eval project."""
|
||||
target = args.dir.resolve()
|
||||
|
||||
if args.guided:
|
||||
settings = _run_guided_init(target)
|
||||
args.lang = settings["lang"]
|
||||
args.preset = settings["preset"]
|
||||
# We'll use guided settings for enhanced config generation
|
||||
return _write_init_files(target, args, guided_settings=settings)
|
||||
|
||||
return _write_init_files(target, args)
|
||||
|
||||
|
||||
def _write_init_files(
|
||||
target: Path,
|
||||
args: argparse.Namespace,
|
||||
guided_settings: dict | None = None,
|
||||
) -> int:
|
||||
"""Write config and template files to target directory."""
|
||||
ce_dir = target / ".cross-eval"
|
||||
ce_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -417,14 +679,23 @@ def cmd_init(args: argparse.Namespace) -> int:
|
||||
plan_sample = PLAN_SAMPLE_KO if lang == "ko" else PLAN_SAMPLE_EN
|
||||
checklist_sample = CHECKLIST_SAMPLE_KO if lang == "ko" else CHECKLIST_SAMPLE_EN
|
||||
|
||||
files = {
|
||||
".cross-eval/config.yaml": DEFAULT_CONFIG_YAML.format(
|
||||
# Generate config content
|
||||
if guided_settings:
|
||||
config_content = _generate_guided_config(args.preset, lang, guided_settings)
|
||||
else:
|
||||
config_content = DEFAULT_CONFIG_YAML.format(
|
||||
preset=args.preset, language=lang,
|
||||
),
|
||||
".cross-eval/plan.md": plan_sample,
|
||||
".cross-eval/checklist.md": checklist_sample,
|
||||
)
|
||||
|
||||
files: dict[str, str] = {
|
||||
".cross-eval/config.yaml": config_content,
|
||||
}
|
||||
|
||||
# Add templates unless guided mode opted out
|
||||
if not guided_settings or guided_settings.get("create_templates", True):
|
||||
files[".cross-eval/plan.md"] = plan_sample
|
||||
files[".cross-eval/checklist.md"] = checklist_sample
|
||||
|
||||
created = []
|
||||
skipped = []
|
||||
for name, content in files.items():
|
||||
@@ -436,23 +707,67 @@ def cmd_init(args: argparse.Namespace) -> int:
|
||||
created.append(name)
|
||||
|
||||
if created:
|
||||
print(f" 생성: {', '.join(created)}")
|
||||
print(f"\n 생성: {', '.join(created)}")
|
||||
if skipped:
|
||||
print(f" 이미 존재 (건너뜀): {', '.join(skipped)}")
|
||||
|
||||
print(f"\n 파이프라인: {args.preset}")
|
||||
print(f" 언어: {lang}")
|
||||
if guided_settings:
|
||||
print(f" Coder: {guided_settings['coder']}")
|
||||
print(f" Reviewer: {guided_settings['reviewer']}")
|
||||
if guided_settings.get("senior"):
|
||||
print(f" Senior: {guided_settings['senior']}")
|
||||
print(f" 최대 반복: {guided_settings['max_iter']}")
|
||||
print("")
|
||||
print("다음 단계:")
|
||||
print(" 1. .cross-eval/plan.md 에 기획서 작성")
|
||||
print(" 2. .cross-eval/checklist.md 에 체크리스트 작성 (선택)")
|
||||
print(" 3. cross-eval run 으로 실행")
|
||||
print("")
|
||||
print("주의: 에이전트는 기본적으로 파일 읽기/쓰기/실행 권한을 가집니다.")
|
||||
print(" 실행 전에 .cross-eval/config.yaml 을 확인하세요.")
|
||||
print("팁: cross-eval doctor 로 환경 점검을 먼저 하세요.")
|
||||
print(" cross-eval demo 로 동작 방식을 미리 볼 수 있습니다.")
|
||||
return 0
|
||||
|
||||
|
||||
def _generate_guided_config(
|
||||
preset: str,
|
||||
lang: str,
|
||||
settings: dict,
|
||||
) -> str:
|
||||
"""Generate config.yaml content from guided init settings."""
|
||||
coder_name = resolve_agent_shorthand(settings["coder"], "coder")
|
||||
reviewer_name = resolve_agent_shorthand(settings["reviewer"], "reviewer")
|
||||
|
||||
lines = [
|
||||
"# cross-eval 설정 (guided init으로 생성됨)",
|
||||
"",
|
||||
"inputs:",
|
||||
" plan: plan.md",
|
||||
" checklist: checklist.md",
|
||||
"",
|
||||
f"coders: [{coder_name}]",
|
||||
f"reviewers: [{reviewer_name}]",
|
||||
]
|
||||
|
||||
senior = settings.get("senior", "")
|
||||
if senior:
|
||||
senior_name = resolve_agent_shorthand(senior, "senior")
|
||||
lines.append(f"seniors: [{senior_name}]")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
f"pipeline: preset:{preset}",
|
||||
"",
|
||||
f"max_iterations: {settings['max_iter']}",
|
||||
f"language: {lang}",
|
||||
"output_dir: output",
|
||||
"",
|
||||
])
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _read_docs_dir(docs_dir: Path) -> str:
|
||||
"""Read all files in a directory and concatenate with filename headers."""
|
||||
parts: list[str] = []
|
||||
@@ -482,6 +797,16 @@ def _apply_model_override(config, agent_name: str, model: str) -> None:
|
||||
agent.args = new_args
|
||||
|
||||
|
||||
def _apply_phased_iteration_override(config, max_iter: int | None) -> None:
|
||||
"""Apply CLI max-iter to converging phases while preserving setup phases."""
|
||||
if max_iter is None:
|
||||
return
|
||||
|
||||
for phase in config.phases:
|
||||
if any(step.verdict for step in phase.steps):
|
||||
phase.max_iterations = max_iter
|
||||
|
||||
|
||||
def cmd_run(args: argparse.Namespace) -> int:
|
||||
"""Load config, validate, and execute the pipeline."""
|
||||
from cross_eval.config import (
|
||||
@@ -562,7 +887,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
preset = args.preset or "simple"
|
||||
# Determine which preset was configured (from YAML or defaults)
|
||||
if args.preset is None and config.phases:
|
||||
preset = "review-fix" # only phased preset currently
|
||||
preset = config.preset_name if config.preset_name != "custom" else "review-fix"
|
||||
elif args.preset is None and not args.coders and not args.reviewers and not args.seniors:
|
||||
pass # no changes needed
|
||||
inferred_coders, inferred_reviewers, inferred_seniors = _infer_roles(
|
||||
@@ -584,11 +909,12 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
config.preset_name = preset
|
||||
if preset in PHASED_PRESETS:
|
||||
config.phases = PHASED_PRESETS[preset](coders, reviewers, seniors)
|
||||
_apply_phased_iteration_override(config, args.max_iter)
|
||||
config.pipeline = []
|
||||
elif preset in PIPELINE_PRESETS:
|
||||
config.pipeline = PIPELINE_PRESETS[preset](coders, reviewers, seniors)
|
||||
config.phases = []
|
||||
if preset == "review-only" and args.max_iter is None and args.min_iter is None:
|
||||
if preset in {"plan-review", "review-only"} and args.max_iter is None and args.min_iter is None:
|
||||
config.max_iterations = 1
|
||||
|
||||
apply_reasoning_effort_settings(
|
||||
@@ -603,10 +929,10 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
if args.model is not None:
|
||||
for agent_name in config.agents:
|
||||
_apply_model_override(config, agent_name, args.model)
|
||||
# --generator-model / --reviewer-model: apply by role
|
||||
if args.generator_model is not None:
|
||||
# --coder-model / --reviewer-model: apply by role
|
||||
if args.coder_model is not None:
|
||||
for coder_name in config.coders:
|
||||
_apply_model_override(config, coder_name, args.generator_model)
|
||||
_apply_model_override(config, coder_name, args.coder_model)
|
||||
if args.reviewer_model is not None:
|
||||
for reviewer_name in config.reviewers:
|
||||
_apply_model_override(config, reviewer_name, args.reviewer_model)
|
||||
@@ -694,6 +1020,11 @@ def cmd_run(args: argparse.Namespace) -> int:
|
||||
if not args.dry_run and result.run_dir:
|
||||
print(f"Output: {result.run_dir}/")
|
||||
|
||||
if result.final_verdict == "ESCALATE":
|
||||
from cross_eval.report import print_escalation_report
|
||||
print_escalation_report(config, result)
|
||||
return 2
|
||||
|
||||
return 0 if result.final_verdict == "PASS" else 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user