Make plan-review a review-fix-verify loop

This commit is contained in:
chungyeong
2026-03-15 00:01:26 +09:00
parent 60c7b07939
commit a85a490a9b
7 changed files with 289 additions and 73 deletions

View File

@@ -53,7 +53,7 @@ agents:
# 방법 1: 프리셋 사용 (사용자가 pipeline YAML 직접 작성할 필요 없음) # 방법 1: 프리셋 사용 (사용자가 pipeline YAML 직접 작성할 필요 없음)
pipeline: preset:simple # "A 생성 → B 리뷰" (기본값) pipeline: preset:simple # "A 생성 → B 리뷰" (기본값)
# pipeline: preset:cross-review # "둘 다 생성 → 서로 리뷰" # pipeline: preset:cross-review # "둘 다 생성 → 서로 리뷰"
# pipeline: preset:plan-review # "구현 전 문서/기획 검토" # pipeline: preset:plan-review # "구현 전 문서 리뷰 → 수정 → 재검증 반복"
# pipeline: preset:coding-review-fix # "초기 코딩 1회 → 리뷰/수정 반복" # pipeline: preset:coding-review-fix # "초기 코딩 1회 → 리뷰/수정 반복"
# 방법 2: 직접 커스텀 (고급 사용자용) # 방법 2: 직접 커스텀 (고급 사용자용)
@@ -77,7 +77,7 @@ pipeline: preset:simple # "A 생성 → B 리뷰" (기본값)
|--------|------|-------------------| |--------|------|-------------------|
| `simple` | A 코딩 → B 리뷰 | coding(agent1) → review(agent2) | | `simple` | A 코딩 → B 리뷰 | coding(agent1) → review(agent2) |
| `cross-review` | 둘 다 코딩, 서로 리뷰 | coding_a → coding_b → review_of_b(agent_a) → review_of_a(agent_b) | | `cross-review` | 둘 다 코딩, 서로 리뷰 | coding_a → coding_b → review_of_b(agent_a) → review_of_a(agent_b) |
| `plan-review` | 구현 전 문서 검토 | parallel plan_review_* → senior_review(optional) | | `plan-review` | 구현 전 문서 리뷰/수정/재검증 반복 | plan_review_* → aggregate_review → plan_fix → verify |
| `coding-review-fix` | 초기 코딩 후 리뷰/수정 반복 | initial_coding(coding) → review_fix(review* → aggregate → coding → verify) | | `coding-review-fix` | 초기 코딩 후 리뷰/수정 반복 | initial_coding(coding) → review_fix(review* → aggregate → coding → verify) |
프리셋은 내부적으로 적절한 pipeline steps + context_override를 자동 구성한다. agents에 정의된 순서대로 agent1, agent2가 배정된다. 프리셋이 불충분하면 직접 steps를 작성할 수 있다. 프리셋은 내부적으로 적절한 pipeline steps + context_override를 자동 구성한다. agents에 정의된 순서대로 agent1, agent2가 배정된다. 프리셋이 불충분하면 직접 steps를 작성할 수 있다.
@@ -185,3 +185,6 @@ final-report.md 생성
--reviewer-effort high \ --reviewer-effort high \
--senior-effort xhigh \ --senior-effort xhigh \
--max-iter 10 --max-iter 10
cross-eval run --plan /Users/chungyeong/Desktop/Dev/cross-eval/UX_IMPROVEMENT_PLAN.md --coder claude --reviewer claude --senior claude --model sonnet --preset coding-review-fix --lang ko --max-iter 1

View File

@@ -112,7 +112,7 @@ pipeline: preset:simple
|--------|------| |--------|------|
| `simple` | Agent A가 코딩, Agent B가 리뷰 (기본값) | | `simple` | Agent A가 코딩, Agent B가 리뷰 (기본값) |
| `cross-review` | 둘 다 코딩, 서로 교차 리뷰 | | `cross-review` | 둘 다 코딩, 서로 교차 리뷰 |
| `plan-review` | 구현 전 기획서/체크리스트/참고문서를 검토하고 필요시 현재 코드베이스와의 정합성도 확인 | | `plan-review` | 구현 전 기획서/체크리스트/참고문서를 검토하고 문서를 수정한 뒤 재검증까지 반복 |
| `review-only` | 기존 코드만 감사 용도로 검토 | | `review-only` | 기존 코드만 감사 용도로 검토 |
| `review-fix` | 리뷰 결과를 취합한 뒤 자동 수정과 재검증까지 반복 | | `review-fix` | 리뷰 결과를 취합한 뒤 자동 수정과 재검증까지 반복 |
| `coding-review-fix` | 초기 코딩 1회 후 리뷰 결과를 취합해 자동 수정과 재검증을 반복 | | `coding-review-fix` | 초기 코딩 1회 후 리뷰 결과를 취합해 자동 수정과 재검증을 반복 |
@@ -120,6 +120,6 @@ pipeline: preset:simple
```bash ```bash
# 초기화 옵션 # 초기화 옵션
cross-eval init --preset cross-review # 교차 리뷰 프리셋 cross-eval init --preset cross-review # 교차 리뷰 프리셋
cross-eval init --preset plan-review # 구현 전 문서 검토 프리셋 cross-eval init --preset plan-review # 문서 리뷰/수정/재검증 프리셋
cross-eval init --lang en # 영어 템플릿 cross-eval init --lang en # 영어 템플릿
``` ```

View File

@@ -205,7 +205,7 @@ def main(argv: list[str] | None = None) -> int:
], ],
help=( help=(
"파이프라인 종류 (기본: simple). " "파이프라인 종류 (기본: simple). "
"simple=코딩+리뷰, cross-review=교차리뷰, plan-review=문서기획검토, " "simple=코딩+리뷰, cross-review=교차리뷰, plan-review=문서리뷰수정재검증, "
"review-only=리뷰만, review-fix=리뷰수렴+자동수정, " "review-only=리뷰만, review-fix=리뷰수렴+자동수정, "
"coding-review-fix=초기코딩후리뷰수렴" "coding-review-fix=초기코딩후리뷰수렴"
), ),
@@ -291,8 +291,8 @@ def main(argv: list[str] | None = None) -> int:
" │ coding- │ 3단계 파이프라인: │\n" " │ coding- │ 3단계 파이프라인: │\n"
" │ review-fix │ 초기 코딩 1회 → 리뷰 취합 → 수정 → 재검증 반복 │\n" " │ review-fix │ 초기 코딩 1회 → 리뷰 취합 → 수정 → 재검증 반복 │\n"
" ├──────────────┼─────────────────────────────────────────────────────┤\n" " ├──────────────┼─────────────────────────────────────────────────────┤\n"
" │ plan-review │ 구현 전 기획서/체크리스트/문서를 검토 \n" " │ plan-review │ 구현 전 기획서/체크리스트/문서를 검토하고\n"
" │ │ 필요하면 현재 코드베이스와의 정합성도 점검\n" " │ │ 수정한 뒤 시니어가 재검증할 때까지 반복 \n"
" ├──────────────┼─────────────────────────────────────────────────────┤\n" " ├──────────────┼─────────────────────────────────────────────────────┤\n"
" │ review-only │ 코드 작성 없이 Reviewer N명이 기존 코드만 검토 │\n" " │ review-only │ 코드 작성 없이 Reviewer N명이 기존 코드만 검토 │\n"
" │ │ (이미 작성된 코드의 품질 감사용) │\n" " │ │ (이미 작성된 코드의 품질 감사용) │\n"
@@ -341,9 +341,9 @@ def main(argv: list[str] | None = None) -> int:
" cross-eval run --plan plan.md --preset review-only \\\n" " cross-eval run --plan plan.md --preset review-only \\\n"
" --reviewer claude --reviewer codex\n" " --reviewer claude --reviewer codex\n"
"\n" "\n"
" 구현 전 문서/기획 검토 (plan-review):\n" " 문서 리뷰 + 수정 + 재검증 반복 (plan-review):\n"
" cross-eval run --plan plan.md --preset plan-review \\\n" " cross-eval run --plan plan.md --preset plan-review \\\n"
" --reviewer claude --reviewer codex\n" " --coder codex --reviewer codex\n"
"\n" "\n"
" 모델 변경:\n" " 모델 변경:\n"
" cross-eval run --plan plan.md --model sonnet\n" " cross-eval run --plan plan.md --model sonnet\n"
@@ -563,7 +563,7 @@ _PRESET_DESCRIPTIONS = {
"simple": "코딩 + 리뷰 (가장 기본)", "simple": "코딩 + 리뷰 (가장 기본)",
"review-fix": "리뷰 → 취합 → 수정 → 재검증 반복", "review-fix": "리뷰 → 취합 → 수정 → 재검증 반복",
"coding-review-fix": "초기 코딩 + 리뷰 수렴 반복", "coding-review-fix": "초기 코딩 + 리뷰 수렴 반복",
"plan-review": "구현 전 기획서/문서 검토", "plan-review": "문서 리뷰 → 수정 → 재검증 반복",
"review-only": "기존 코드만 리뷰 (코딩 없음)", "review-only": "기존 코드만 리뷰 (코딩 없음)",
"cross-review": "2명이 각각 구현 후 교차 리뷰", "cross-review": "2명이 각각 구현 후 교차 리뷰",
} }
@@ -929,7 +929,7 @@ def cmd_run(args: argparse.Namespace) -> int:
elif preset in PIPELINE_PRESETS: elif preset in PIPELINE_PRESETS:
config.pipeline = PIPELINE_PRESETS[preset](coders, reviewers, seniors) config.pipeline = PIPELINE_PRESETS[preset](coders, reviewers, seniors)
config.phases = [] config.phases = []
if preset in {"plan-review", "review-only"} and args.max_iter is None and args.min_iter is None: if preset == "review-only" and args.max_iter is None and args.min_iter is None:
config.max_iterations = 1 config.max_iterations = 1
sync_phased_iterations(config) sync_phased_iterations(config)

View File

@@ -31,7 +31,7 @@ DEFAULT_ROLE_REASONING_EFFORTS = {
"reviewer": "medium", "reviewer": "medium",
"senior": "high", "senior": "high",
} }
FIX_STYLE_PRESETS = {"review-fix", "coding-review-fix"} FIX_STYLE_PRESETS = {"plan-review", "review-fix", "coding-review-fix"}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -296,7 +296,11 @@ def _default_seniors_for_preset(
"""Infer a default senior agent for presets that benefit from adjudication.""" """Infer a default senior agent for presets that benefit from adjudication."""
if not ( if not (
isinstance(pipeline_raw, str) isinstance(pipeline_raw, str)
and pipeline_raw in {"preset:review-fix", "preset:coding-review-fix"} and pipeline_raw in {
"preset:plan-review",
"preset:review-fix",
"preset:coding-review-fix",
}
and reviewers and reviewers
): ):
return [] return []

View File

@@ -472,12 +472,58 @@ PLAN_REVIEW_TEMPLATE_KO = """\
그렇지 않으면: VERDICT: FAIL 그렇지 않으면: VERDICT: FAIL
""" """
PLAN_FIX_TEMPLATE = """\
You are tasked with revising planning documents based on adjudicated review feedback.
## Artifact References
{artifact_references}
## Current Review Feedback
{feedback}
## Instructions
1. Read the referenced plan/checklist/docs/review artifacts directly from disk.
2. Update the planning package itself: the plan, checklist, and reference documents as needed.
3. Do NOT write or modify production code. Only revise planning artifacts.
4. Address ONLY the confirmed planning issues from the current review feedback.
5. If feedback marks any item as DISMISSED or false positive, leave it unchanged.
6. Make the smallest document changes that resolve ambiguity, omissions, scope creep, or repository compatibility issues.
7. Keep the plan, checklist, and supporting docs internally consistent after your edits.
8. After editing, briefly summarize what you changed and any blocker that still needs human input.
"""
PLAN_FIX_TEMPLATE_KO = """\
당신은 시니어 리뷰 결과를 바탕으로 기획 문서를 수정하는 담당자입니다.
## 참조 아티팩트
{artifact_references}
## 현재 리뷰 피드백
{feedback}
## 지침
1. 참조된 plan/checklist/docs/review markdown를 직접 읽으세요.
2. 수정 대상은 기획 패키지 자체입니다. 필요에 따라 기획서, 체크리스트, 참고 문서를 수정하세요.
3. 프로덕션 코드를 작성하거나 수정하지 마세요. 기획 문서만 고치세요.
4. 현재 리뷰 피드백에서 확정된 기획 이슈만 해결하세요.
5. DISMISSED 또는 오탐으로 정리된 항목은 건드리지 마세요.
6. 모호성, 누락, 과도한 범위, 저장소 정합성 문제를 해소하는 최소한의 문서 수정만 하세요.
7. 수정 후에도 기획서, 체크리스트, 참고 문서가 서로 모순되지 않게 유지하세요.
8. 수정이 끝나면 무엇을 바꿨는지와 아직 사람 판단이 필요한 blocker가 있는지 짧게 정리하세요.
"""
AGGREGATE_REVIEW_TEMPLATE = """\ AGGREGATE_REVIEW_TEMPLATE = """\
You are adjudicating multiple review results and turning them into an actionable decision. You are adjudicating multiple review results and turning them into an actionable decision.
## Artifact References ## Artifact References
{artifact_references} {artifact_references}
## Candidate Artifact Under Review
{candidate_outputs}
## Reviewer Findings Bundle
{reviews_bundle}
## Previous Issue Tracker ## Previous Issue Tracker
{previous_senior_tracker} {previous_senior_tracker}
@@ -486,19 +532,19 @@ You are adjudicating multiple review results and turning them into an actionable
## Instructions ## Instructions
Read the referenced plan/checklist/docs/review artifacts directly from disk. \ Read the referenced plan/checklist/docs/review artifacts directly from disk. \
Explore the project directory and the referenced git commit/diff to confirm the \ Inspect the repository and referenced artifacts only as needed to confirm the \
current codebase state. Use the execution evidence above to verify claims against \ current target state. Use the execution evidence above to verify claims against \
actual command outputs, artifact paths, and exit codes. Then: actual command outputs, artifact paths, and exit codes. Then:
1. Deduplicate overlapping issues across reviewers. 1. Deduplicate overlapping issues across reviewers.
2. Resolve disagreements explicitly. 2. Resolve disagreements explicitly.
3. Keep only issues supported by the plan, checklist, code, or reviewer evidence. 3. Keep only issues supported by the plan, checklist, reference docs, repository state, or reviewer evidence.
4. When evidence is mixed, explain what was confirmed, what was dismissed, and what still needs follow-up. 4. When evidence is mixed, explain what was confirmed, what was dismissed, and what still needs follow-up.
5. Produce a prioritized action list for the coder. 5. Produce a prioritized action list for the implementer/editor.
6. Maintain the Issue Tracker table across iterations (carry forward unresolved issues). 6. Maintain the Issue Tracker table across iterations (carry forward unresolved issues).
7. If no confirmed issue remains, output VERDICT: PASS. 7. If no confirmed issue remains, output VERDICT: PASS.
8. If issues exist that the coder can fix, output VERDICT: FAIL. 8. If issues exist that the implementer/editor can fix, output VERDICT: FAIL.
9. If issues require human intervention (ambiguous requirements, architecture decisions, \ 9. If issues require human intervention (ambiguous requirements, architecture decisions, \
external dependency problems, or the same issue persists after 2+ fix attempts), \ external dependency problems, or the same issue persists after 2+ attempts), \
output VERDICT: ESCALATE. output VERDICT: ESCALATE.
## Output Format ## Output Format
@@ -512,8 +558,8 @@ output VERDICT: ESCALATE.
(Write "None" if nothing was dismissed.) (Write "None" if nothing was dismissed.)
### Action Items ### Action Items
1. Concrete fix the coder should make 1. Concrete fix the implementer/editor should make
2. Concrete fix the coder should make 2. Concrete fix the implementer/editor should make
## Issue Tracker ## Issue Tracker
@@ -536,6 +582,12 @@ AGGREGATE_REVIEW_TEMPLATE_KO = """\
## 참조 아티팩트 ## 참조 아티팩트
{artifact_references} {artifact_references}
## 현재 검토 대상
{candidate_outputs}
## 리뷰 결과 묶음
{reviews_bundle}
## 이전 이슈 트래커 ## 이전 이슈 트래커
{previous_senior_tracker} {previous_senior_tracker}
@@ -543,17 +595,17 @@ AGGREGATE_REVIEW_TEMPLATE_KO = """\
{execution_evidence} {execution_evidence}
## 지침 ## 지침
참조된 plan/checklist/docs/review markdown와 git 상태를 직접 읽어 현재 코드베이스 상태를 확인한 뒤, \ 참조된 plan/checklist/docs/review markdown와 저장소 상태를 직접 읽어 현재 검토 대상의 상태를 확인한 뒤, \
위 실행 증거를 활용하여 에이전트의 주장을 실제 명령어 출력, 아티팩트 경로, 종료 코드로 검증하세요. \ 위 실행 증거를 활용하여 에이전트의 주장을 실제 명령어 출력, 아티팩트 경로, 종료 코드로 검증하세요. \
그런 다음 아래를 수행하세요. 그런 다음 아래를 수행하세요.
1. 리뷰어들 사이에 중복되는 이슈를 합치세요. 1. 리뷰어들 사이에 중복되는 이슈를 합치세요.
2. 의견 충돌은 명시적으로 정리하세요. 2. 의견 충돌은 명시적으로 정리하세요.
3. 기획서, 체크리스트, 코드, 리뷰 근거로 뒷받침되는 이슈만 남기세요. 3. 기획서, 체크리스트, 참고 문서, 저장소 상태, 리뷰 근거로 뒷받침되는 이슈만 남기세요.
4. 근거가 엇갈리면 무엇이 확정이고 무엇이 기각 또는 추가확인 대상인지 분명히 적으세요. 4. 근거가 엇갈리면 무엇이 확정이고 무엇이 기각 또는 추가확인 대상인지 분명히 적으세요.
5. coder가 바로 수정할 수 있는 우선순위 액션 아이템을 만드세요. 5. 수정 담당자가 바로 처리할 수 있는 우선순위 액션 아이템을 만드세요.
6. 이슈 트래커 테이블을 반복 간에 유지하세요 (미해결 이슈를 이월). 6. 이슈 트래커 테이블을 반복 간에 유지하세요 (미해결 이슈를 이월).
7. 확정된 이슈가 없으면 VERDICT: PASS 를 출력하세요. 7. 확정된 이슈가 없으면 VERDICT: PASS 를 출력하세요.
8. coder가 수정 가능한 이슈가 있으면 VERDICT: FAIL 을 출력하세요. 8. 수정 담당자가 해결 가능한 이슈가 있으면 VERDICT: FAIL 을 출력하세요.
9. 사람의 개입이 필요한 이슈(모호한 요구사항, 아키텍처 결정, 외부 의존성 문제, \ 9. 사람의 개입이 필요한 이슈(모호한 요구사항, 아키텍처 결정, 외부 의존성 문제, \
동일 이슈가 2회 이상 해결 실패)가 있으면 VERDICT: ESCALATE 를 출력하세요. 동일 이슈가 2회 이상 해결 실패)가 있으면 VERDICT: ESCALATE 를 출력하세요.
@@ -568,8 +620,8 @@ AGGREGATE_REVIEW_TEMPLATE_KO = """\
(기각된 항목이 없으면 "없음"이라고 작성하세요.) (기각된 항목이 없으면 "없음"이라고 작성하세요.)
### 액션 아이템 ### 액션 아이템
1. coder가 수정해야 할 구체적인 작업 1. 수정 담당자가 처리해야 할 구체적인 작업
2. coder가 수정해야 할 구체적인 작업 2. 수정 담당자가 처리해야 할 구체적인 작업
## 이슈 트래커 ## 이슈 트래커
@@ -592,6 +644,7 @@ DEFAULT_TEMPLATES: dict[str, dict[str, str]] = {
"coding": CODING_TEMPLATE, "coding": CODING_TEMPLATE,
"review": REVIEW_TEMPLATE, "review": REVIEW_TEMPLATE,
"plan-review": PLAN_REVIEW_TEMPLATE, "plan-review": PLAN_REVIEW_TEMPLATE,
"plan-fix": PLAN_FIX_TEMPLATE,
"review-only": REVIEW_ONLY_TEMPLATE, "review-only": REVIEW_ONLY_TEMPLATE,
"aggregate-review": AGGREGATE_REVIEW_TEMPLATE, "aggregate-review": AGGREGATE_REVIEW_TEMPLATE,
}, },
@@ -599,6 +652,7 @@ DEFAULT_TEMPLATES: dict[str, dict[str, str]] = {
"coding": CODING_TEMPLATE_KO, "coding": CODING_TEMPLATE_KO,
"review": REVIEW_TEMPLATE_KO, "review": REVIEW_TEMPLATE_KO,
"plan-review": PLAN_REVIEW_TEMPLATE_KO, "plan-review": PLAN_REVIEW_TEMPLATE_KO,
"plan-fix": PLAN_FIX_TEMPLATE_KO,
"review-only": REVIEW_ONLY_TEMPLATE_KO, "review-only": REVIEW_ONLY_TEMPLATE_KO,
"aggregate-review": AGGREGATE_REVIEW_TEMPLATE_KO, "aggregate-review": AGGREGATE_REVIEW_TEMPLATE_KO,
}, },
@@ -843,56 +897,75 @@ def _build_review_only_preset(
def _build_plan_review_preset( def _build_plan_review_preset(
coders: list[str], reviewers: list[str], seniors: list[str], coders: list[str], reviewers: list[str], seniors: list[str],
) -> list[StepConfig]: ) -> list[StepConfig]:
"""Plan-review: reviewers audit planning docs before implementation.""" """Plan-review: review planning docs, revise them, then verify in a loop."""
if not coders:
raise ValueError("'plan-review' preset requires at least 1 coder")
if not reviewers: if not reviewers:
raise ValueError("'plan-review' preset requires at least 1 reviewer") raise ValueError("'plan-review' preset requires at least 1 reviewer")
if len(reviewers) == 1 and not seniors: review_steps: list[StepConfig] = []
return [ if len(reviewers) == 1:
review_steps.append(
StepConfig( StepConfig(
name="plan_review", name="plan_review",
agent=reviewers[0], agent=reviewers[0],
role="review", role="review",
prompt_template="default:plan-review", prompt_template="default:plan-review",
output_key="plan_review_result", output_key="plan_review_result",
verdict=True,
), ),
] )
review_step_names = ["plan_review"]
steps: list[StepConfig] = [] review_output_keys = ["plan_review_result"]
else:
reviewer_keys = _unique_safe_keys(reviewers) reviewer_keys = _unique_safe_keys(reviewers)
for reviewer, rk in zip(reviewers, reviewer_keys): for reviewer, rk in zip(reviewers, reviewer_keys):
steps.append( review_steps.append(
StepConfig( StepConfig(
name=f"plan_review_{rk}", name=f"plan_review_{rk}",
agent=reviewer, agent=reviewer,
role="review", role="review",
prompt_template="default:plan-review", prompt_template="default:plan-review",
output_key=f"plan_review_{rk}", output_key=f"plan_review_{rk}",
verdict=not seniors,
parallel=True, parallel=True,
), ),
) )
if seniors: review_step_names = [f"plan_review_{rk}" for rk in reviewer_keys]
step_names = [f"plan_review_{rk}" for rk in reviewer_keys] review_output_keys = [f"plan_review_{rk}" for rk in reviewer_keys]
output_keys = [f"plan_review_{rk}" for rk in reviewer_keys]
steps.append( fix_coder = coders[0]
senior_agent = seniors[0] if seniors else reviewers[0]
return review_steps + [
StepConfig( StepConfig(
name="senior_review", name="aggregate_review",
agent=seniors[0], agent=senior_agent,
role="review", role="review",
prompt_template="default:aggregate-review", prompt_template="default:aggregate-review",
output_key="senior_review_result", output_key="aggregate_review",
verdict=True,
context_override={ context_override={
"candidate_outputs": "Planning documents under review (plan/checklist/reference docs).", "candidate_outputs": "Current planning package under review (plan/checklist/reference docs).",
"reviews_bundle": _build_named_bundle( "reviews_bundle": _build_named_bundle(
reviewers, step_names, output_keys, "Review", reviewers, review_step_names, review_output_keys, "Review",
), ),
}, },
), ),
) StepConfig(
return steps name="plan_fix",
agent=fix_coder,
role="coding",
prompt_template="default:plan-fix",
output_key="plan_fix_output",
context_override={"feedback": "{aggregate_review}"},
),
StepConfig(
name="verify",
agent=senior_agent,
role="review",
prompt_template="default:plan-review",
output_key="verify_result",
verdict=True,
),
]
def _build_review_fix_preset( def _build_review_fix_preset(

View File

@@ -42,6 +42,8 @@ from cross_eval.prompts import (
REVIEW_TEMPLATE_KO, REVIEW_TEMPLATE_KO,
PLAN_REVIEW_TEMPLATE, PLAN_REVIEW_TEMPLATE,
PLAN_REVIEW_TEMPLATE_KO, PLAN_REVIEW_TEMPLATE_KO,
PLAN_FIX_TEMPLATE,
PLAN_FIX_TEMPLATE_KO,
REVIEW_ONLY_TEMPLATE, REVIEW_ONLY_TEMPLATE,
REVIEW_ONLY_TEMPLATE_KO, REVIEW_ONLY_TEMPLATE_KO,
AGGREGATE_REVIEW_TEMPLATE, AGGREGATE_REVIEW_TEMPLATE,
@@ -310,7 +312,23 @@ class BuiltinAgentConfigTest(unittest.TestCase):
self.assertIn("Repeated Aggregate Findings", report) self.assertIn("Repeated Aggregate Findings", report)
self.assertIn("same as iteration 3", report) self.assertIn("same as iteration 3", report)
def test_review_fix_defaults_senior_from_reviewer_family(self) -> None: def test_fix_and_plan_presets_default_senior_from_reviewer_family(self) -> None:
self.assertEqual(
_default_seniors_for_preset(
"preset:plan-review",
["codex-reviewer"],
BUILTIN_AGENTS,
),
["codex-senior"],
)
self.assertEqual(
_default_seniors_for_preset(
"preset:plan-review",
["claude-reviewer"],
BUILTIN_AGENTS,
),
["claude-senior"],
)
self.assertEqual( self.assertEqual(
_default_seniors_for_preset( _default_seniors_for_preset(
"preset:review-fix", "preset:review-fix",
@@ -421,23 +439,49 @@ class BuiltinAgentConfigTest(unittest.TestCase):
) )
self.assertEqual( self.assertEqual(
[step.output_key for step in steps], [step.output_key for step in steps[:2]],
["plan_review_codex_reviewer", "plan_review_codex_reviewer_2"], ["plan_review_codex_reviewer", "plan_review_codex_reviewer_2"],
) )
def test_plan_review_with_senior_adds_aggregate_step(self) -> None: def test_plan_review_builds_review_fix_verify_loop(self) -> None:
steps = _build_plan_review_preset( steps = _build_plan_review_preset(
["codex-coder"], ["codex-coder"],
["claude-reviewer", "codex-reviewer"], ["claude-reviewer", "codex-reviewer"],
["claude-senior"], ["claude-senior"],
) )
self.assertEqual(steps[-1].name, "senior_review") self.assertEqual(
self.assertEqual(steps[-1].agent, "claude-senior") [step.name for step in steps],
self.assertTrue(steps[-1].verdict) [
"plan_review_claude_reviewer",
"plan_review_codex_reviewer",
"aggregate_review",
"plan_fix",
"verify",
],
)
self.assertEqual(steps[2].agent, "claude-senior")
self.assertEqual(steps[3].agent, "codex-coder")
self.assertEqual(steps[4].agent, "claude-senior")
self.assertTrue(steps[4].verdict)
self.assertFalse(steps[0].verdict) self.assertFalse(steps[0].verdict)
self.assertFalse(steps[1].verdict) self.assertFalse(steps[1].verdict)
def test_plan_review_single_reviewer_uses_default_loop_steps(self) -> None:
steps = _build_plan_review_preset(
["codex-coder"],
["codex-reviewer"],
[],
)
self.assertEqual(
[step.name for step in steps],
["plan_review", "aggregate_review", "plan_fix", "verify"],
)
self.assertEqual(steps[1].agent, "codex-reviewer")
self.assertEqual(steps[2].prompt_template, "default:plan-fix")
self.assertTrue(steps[3].verdict)
def test_cross_review_duplicate_coders_get_unique_step_keys(self) -> None: def test_cross_review_duplicate_coders_get_unique_step_keys(self) -> None:
steps = _build_cross_review_preset( steps = _build_cross_review_preset(
["codex-coder", "codex-coder"], ["codex-coder", "codex-coder"],
@@ -576,6 +620,8 @@ class PromptTemplateTest(unittest.TestCase):
"""Coding templates should tell coder to ignore DISMISSED items.""" """Coding templates should tell coder to ignore DISMISSED items."""
self.assertIn("DISMISSED", CODING_TEMPLATE) self.assertIn("DISMISSED", CODING_TEMPLATE)
self.assertIn("DISMISSED", CODING_TEMPLATE_KO) self.assertIn("DISMISSED", CODING_TEMPLATE_KO)
self.assertIn("DISMISSED", PLAN_FIX_TEMPLATE)
self.assertIn("DISMISSED", PLAN_FIX_TEMPLATE_KO)
def test_aggregate_templates_dismissed_structure(self) -> None: def test_aggregate_templates_dismissed_structure(self) -> None:
"""Aggregate templates should use [False positive] / [Already fixed] tags.""" """Aggregate templates should use [False positive] / [Already fixed] tags."""
@@ -583,6 +629,10 @@ class PromptTemplateTest(unittest.TestCase):
self.assertIn("[Already fixed]", AGGREGATE_REVIEW_TEMPLATE) self.assertIn("[Already fixed]", AGGREGATE_REVIEW_TEMPLATE)
self.assertIn("[오탐]", AGGREGATE_REVIEW_TEMPLATE_KO) self.assertIn("[오탐]", AGGREGATE_REVIEW_TEMPLATE_KO)
self.assertIn("[수정 완료]", AGGREGATE_REVIEW_TEMPLATE_KO) self.assertIn("[수정 완료]", AGGREGATE_REVIEW_TEMPLATE_KO)
self.assertIn("{candidate_outputs}", AGGREGATE_REVIEW_TEMPLATE)
self.assertIn("{reviews_bundle}", AGGREGATE_REVIEW_TEMPLATE)
self.assertIn("{candidate_outputs}", AGGREGATE_REVIEW_TEMPLATE_KO)
self.assertIn("{reviews_bundle}", AGGREGATE_REVIEW_TEMPLATE_KO)
class ReviewMetricsParsingTest(unittest.TestCase): class ReviewMetricsParsingTest(unittest.TestCase):
@@ -1033,6 +1083,34 @@ class FixPresetBehaviorTest(unittest.TestCase):
self.assertTrue(captured["agentic"]) self.assertTrue(captured["agentic"])
self.assertEqual(captured["phase_max"], 3) self.assertEqual(captured["phase_max"], 3)
def test_run_preset_plan_review_auto_enables_agentic_without_flag(self) -> None:
captured: dict[str, object] = {}
def _fake_run_pipeline(config, **kwargs):
captured["preset"] = config.preset_name
captured["agentic"] = config.agents[config.coders[0]].agentic
captured["seniors"] = list(config.seniors)
captured["steps"] = [step.name for step in config.pipeline]
captured["max_iter"] = config.max_iterations
return PipelineResult(
iterations=[],
final_verdict="PASS",
run_dir=Path(".cross-eval/output"),
)
with patch("cross_eval.pipeline.run_pipeline", side_effect=_fake_run_pipeline):
exit_code = main(["run", "--preset", "plan-review", "--dry-run"])
self.assertEqual(exit_code, 0)
self.assertEqual(captured["preset"], "plan-review")
self.assertTrue(captured["agentic"])
self.assertEqual(captured["seniors"], ["claude-senior"])
self.assertEqual(
captured["steps"],
["plan_review", "aggregate_review", "plan_fix", "verify"],
)
self.assertEqual(captured["max_iter"], 3)
def test_run_senior_model_override_applies_only_to_seniors(self) -> None: def test_run_senior_model_override_applies_only_to_seniors(self) -> None:
captured: dict[str, list[str]] = {} captured: dict[str, list[str]] = {}

View File

@@ -13,7 +13,11 @@ from cross_eval.models import (
StepConfig, StepConfig,
) )
from cross_eval.pipeline import run_pipeline from cross_eval.pipeline import run_pipeline
from cross_eval.prompts import _build_review_fix_preset, _build_simple_preset from cross_eval.prompts import (
_build_plan_review_preset,
_build_review_fix_preset,
_build_simple_preset,
)
def _make_mock_agent(outputs: list[str]): def _make_mock_agent(outputs: list[str]):
@@ -262,6 +266,60 @@ class TestPhasedPipelineEscalateBreaksPhase(unittest.TestCase):
self.assertTrue(len(result.escalated_issues) > 0) self.assertTrue(len(result.escalated_issues) > 0)
class TestPlanReviewPipelineLoopsUntilVerifyPass(unittest.TestCase):
"""Document plan-review should revise docs and re-verify across iterations."""
def test_plan_review_fail_then_pass(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
coders = ["claude-coder"]
reviewers = ["claude-reviewer"]
seniors = ["claude-senior"]
steps = _build_plan_review_preset(coders, reviewers, seniors)
config = PipelineConfig(
output_dir=Path(tmpdir),
max_iterations=4,
min_iterations=1,
language="en",
inputs={
"plan": "Test plan",
"checklist": "Test checklist",
"docs": "Reference docs",
},
agents=dict(BUILTIN_AGENTS),
coders=coders,
reviewers=reviewers,
seniors=seniors,
pipeline=steps,
preset_name="plan-review",
)
mock = _make_step_mock({
"plan_review": [
"Requirements are ambiguous\n\nVERDICT: FAIL",
"Looks aligned\n\nVERDICT: PASS",
],
"aggregate_review": [
"### Confirmed Issues\n- Clarify acceptance criteria\n\n"
"### Action Items\n1. Tighten the checklist\n\nVERDICT: FAIL",
"### Confirmed Issues\nNone\n\n"
"### Dismissed Findings\nNone\n\n"
"### Action Items\n1. No document changes needed\n\nVERDICT: PASS",
],
"plan_fix": ["Updated plan and checklist", "No-op"],
"verify": [
"Still missing edge-case criteria\n\nVERDICT: FAIL",
"Planning package is now implementable\n\nVERDICT: PASS",
],
})
with patch("cross_eval.pipeline.invoke_agent", side_effect=mock):
result = run_pipeline(config)
self.assertEqual(result.final_verdict, "PASS")
self.assertEqual(len(result.iterations), 2)
class TestAutoEscalateFiresWithoutSenior(unittest.TestCase): class TestAutoEscalateFiresWithoutSenior(unittest.TestCase):
"""Test 6: simple pipeline without senior, same FAIL feedback 3 times -> auto-escalate.""" """Test 6: simple pipeline without senior, same FAIL feedback 3 times -> auto-escalate."""