Issue #25: a promoted plan is reused only with --plan-file; the implicit newest-plan load is gone in both runners, and an empty planner report no longer passes silently. VERSIONS.md carries the entry; the four merge-side review reports and their identity sidecars (base6fc9194-> head1e82651, evidence review-tree) are archived under .agents/reports/<session>/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
13 KiB
🔍 Review Report — PR #32 병합 전 독립 리뷰 (job 293e905f)
- Reviewer:
planner-reviewer-claude-01(role: planner,reviewer) · 2026-09-22 - 대상: PR #32
fix/issue-25@1e82651, basemain 6fc9194(v4.7.0). 5커밋, 4파일+486/−31. - 검증 경로:
/var/folders/.../T/mam-review/loop-20260921T150212Z-97d845/1(브리프가 지정한 리뷰 워크트리). 실제로 거기서만 검증했다. - identity 확인: 검증 경로의
HEAD = 1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd,HEAD^{tree} = 4fd82ad1c2682d12c3f5114818f97291af28a032— 브리프의head·tree와 일치한다(항목 5 identity 가 실제로 작동함을 이 잡 자체가 보여준다). - 안전 규칙 준수: 검증 경로·소스 워크트리(
multi-agent-mux-wt/pr-32)·본 체크아웃(main 6fc9194) 모두 수정·커밋·checkout 없음. 실험은 전부 scratchpad 임시 디렉터리(uuid4 이름)에서 했고, pytest 는--basetemp을 검증 경로 밖으로 주었다. 실제.mam·데몬·세션 조작 없음, push·merge·PR 상태 변경 없음, purge·scrub 실행 없음.
결론 요약
병합을 막을 결함은 없다. 핵심 변경(암묵적 계획서 로드 제거, --plan-file 명시 재사용, 네 지점 공통 계획 텍스트 검사)은 코드·문서·테스트가 일치하며, 새 테스트 17건 중 13건이 수정 전 소스에서는 실패한다(직접 확인). 비차단 지적 3건(E1–E3)을 남긴다.
1. 암묵적 로드 제거 (항목 1)
| 확인 | 결과 |
|---|---|
| Python 러너 | runner.py 에서 glob import 가 사라졌고, 남은 glob 사용처는 find_report 의 report-final.md 탐색(:568) 하나뿐이다. .agents/reports/<planner>/plan-*.md 를 읽는 코드가 없다 |
| 레거시 셸 러너 | run_loop.sh 의 EXISTING_PLAN_FILE/ls -t .../plan-*.md 블록이 통째로 사라지고 log_info "No plan loaded; the Creator plans by itself." 만 남았다 |
| 다른 경로 | 저장소 전체에서 plan-*.md 를 읽는 코드는 없다(승격 쪽 쓰기만 runner.py:1368-1372 에 남는다 — 정상) |
| planner 자동 선택 | resolve_planner_session(run_loop.sh:386)·resolve_planner_session 파이썬 대응은 남아 있으나, 그 결과는 plan 모드의 위임 대상과 승격 경로에만 쓰인다. 자기계획 모드에서 planner 이름으로 파일을 찾는 코드는 없다 |
2. --plan-file (항목 2) — 실측
검증 경로의 러너를 임시 제어 루트(WORKSPACE_ROOT=<tmp>/ctl)로 직접 돌려 확인했다.
| 입력 | 결과 |
|---|---|
| 없는 파일 | [✗] --plan-file <절대경로>: not a readable file with plan text in it. · exit 1 |
| 빈 파일 / 공백만 있는 파일 | 같은 메시지 · exit 1 |
| 디렉터리 | 같은 메시지 · exit 1 (is_file() 로 걸린다) |
읽기 권한 없음(chmod 000) |
같은 메시지 · exit 1 (OSError → None) |
--plan 과 함께 |
ERROR: --plan-file cannot be combined with --plan. + 사용 예 2줄 · exit 1 |
| 상대경로 | 제어 루트 기준. 호출 cwd(<tmp>/elsewhere)에 plans/p.md 가 실제로 있어도 러너는 <ctl>/plans/p.md 를 찾고 실패한다 — #25 의 "다른 디렉터리의 동명 파일을 집어오는" 사고를 구조적으로 막는다 |
공백 있는 경로(my plans/p.md, 상대) |
정상 로드: Using the plan given with --plan-file: <ctl>/my plans/p.md (modified ... UTC) (셸을 거치지 않으므로 인용 문제 없음) |
절대경로·~ |
Path.__truediv__ 가 절대경로를 그대로 두고 expanduser() 가 앞서 적용된다(P3d 가 고정) |
--review-only 와 함께 |
경고 후 무시(log_warn), current_plan 을 채우지 않는다 |
레거시 셸(MAM_LOOP_PY=0) |
ERROR: --worktree/--base/--review-only/--review-isolation/--remote-review/--plan-file require the Python loop (MAM_LOOP_PY=1) · exit 1 — 동등 구현 대신 명시적 거부이며, 기본 경로(MAM_LOOP_PY=1)는 exec "$_py" -m lib_py.loop "${MAM_LOOP_ARGV[@]}"(run_loop.sh:262)로 원본 argv 를 그대로 넘기므로 공백 경로도 안전하다 |
읽기가 잡을 하나도 만들기 전(parse_args 단계)에 끝나는 것도 확인했다 — 실패 시 레지스트리에 아무 흔적이 없다.
3. 계획 텍스트 검사 (항목 3)
_plan_text()(runner.py:303-317)가 "읽을 수 있는 일반 파일 + 공백 제외 내용 있음"을 단일 판정으로 만들고, 파이썬 러너의 네 지점이 모두 이를 쓴다: --plan-file(:265), --plan 초기 계획서(:1025), 논의 턴 refine(:1054), 리뷰 에스컬레이션 refine(:1284).
- 빈 초기 계획서 →
log_error후 return 1. 이때 이미 만든 잡은release_guard/정리 경로가 담당하고, 자기계획으로의 조용한 격하가 사라졌다. - 빈 refine → 경고 + 이전 계획 유지. 그래서 "갱신된 다음 계획서에 입각하여…" 프롬프트가 빈 계획을 싣는 일이 없다.
- 에스컬레이션 경로(
[ESCALATE: PLANNER])는if self.plan_mode and complex_fix(:1275)로 plan 모드에만 걸린다.--plan-file로 계획을 실은 실행에서 ESCALATE 판정이 나오면 재계획 없이 일반 수정 라운드로 간다 — 샌드박스에서 직접 구동해 확인했다(Planner 위임 0건, 수정 프롬프트는 "리뷰어들이 지적한 다음 피드백…"). SKILL.md:31 의 "수정 라운드에는 리뷰 피드백만" 서술과 일치한다. 다만 이 동작 자체는 E2 로 남긴다.
4. 체크포인트 (항목 4)
.mam/loop-checkpoint.json 의 current_plan 은 쓰기 전용이다. checkpoint.load_checkpoint() 는 정의되어 있으나 runner.py 어디에서도 호출하지 않는다(grep 'checkpoint\.' → save_checkpoint·remove_checkpoint 뿐). 따라서 본 체크아웃 체크포인트에 남아 있는 Issue #6 계획 텍스트가 다음 루프나 재개에서 되살아날 경로는 없다. 계획은 매 실행마다 --plan-file 또는 --plan 으로만 들어온다.
5. 옵션 없을 때의 동작 변화 (항목 5)
- 자기계획 프롬프트(
runner.py:1095)와 승격 경로(:1367if self.plan_mode and self.current_plan and self.plan_job_id)는 그대로다. 즉--plan-file로 실은 계획서는 승격되지 않는다(승격은 plan 모드의 planner 산출물만) — 옳은 구분이다. - 바뀐 것은 "planner 리포트 디렉터리에서 최신 plan-*.md 를 끌어오던" 동작 하나뿐이다. 그 외 프롬프트 문구·옵션 기본값·종료 코드는 동일하다.
- 기존 스위트 정합은 §6 에서 실측했다.
6. 테스트 (항목 6)
6-1. 검증 경로에서의 실행
cd <검증 경로> && PYTHONDONTWRITEBYTECODE=1 <제어 루트 .venv/bin/python> -m pytest -p no:cacheprovider -q -rfEs \
--basetemp=<검증 경로 밖> tests/test_issue25_plan_reuse.py tests/test_loop_cli.py tests/test_issue5_phase3_loop.py \
tests/test_issue21_loop_b.py tests/test_issue21_phase2_loop_a.py tests/test_issue21_acceptance.py
→ 2 failed, 108 passed in 220.98s (skipped 0)
실패 2건은 PR 회귀가 아니라 리뷰 워크트리에 .venv 가 없어서 생긴 환경 문제다.
| 실패 | 원인 | 확인 |
|---|---|---|
test_loop_cli.py::test_creator_parses_and_reaches_session_check |
run_loop.sh 를 하위 프로세스로 띄우는데, REPO_ROOT(= 검증 경로)에 .venv 가 없어 _py 가 시스템 python 으로 떨어지고 RuntimeError: Failed to eager load lib_py.loop.autocomplete: No module named 'paho' 로 죽는다(run_loop.sh:253) |
AGENT_PYTHON_BIN=<제어 루트 .venv/bin/python> 만 주고 둘을 다시 돌리면 2 passed in 0.34s |
test_issue5_phase3_loop.py::test_p3_02_run_loop_wrapper_delegates_to_python_default |
동일 | 동일 |
두 시험 모두 PR 이 건드리지 않은 파일이며, 실패 메시지에 계획서·--plan-file 은 등장하지 않는다.
6-2. 수정 없이는 실패하는가 — 확인함
검증 경로 사본을 만들고 runner.py·run_loop.sh·SKILL.md 세 파일만 base 6fc9194 판으로 되돌려 새 테스트를 돌렸다(검증 경로 자체는 건드리지 않았다).
13 failed, 4 passed in 1.51s
FAILED P1, P2, P3b, P3c, P3d, P5, P9[empty], P9[whitespace], P10, P11, P7, P7b, P8
통과한 4건(P3·P4·P5b·P6)은 옵션 없는 일반 CLI 오류·plan 모드 정상 경로라 수정과 무관한 것들이다. 즉 새 테스트는 이 변경을 정확히 겨냥한다.
6-3. 격리
| 항목 | 결과 |
|---|---|
실제 .mam/jobs 전후 |
차이 0 |
실제 .agents/reports |
새 항목 없음(테스트는 각자 tmp_path 안에 제어 루트를 만든다 — ctl 픽스처) |
| 검증 경로 tracked 파일 | 변화 없음(git status --short 클린, HEAD 1e82651 유지) |
| 검증 경로 ignored | 내 실행이 .mam/(무시됨)을 만들었다. tracked 파일·커밋에는 영향이 없으며, 지우면 리뷰 트리를 수정하는 셈이라 그대로 두었다 → E3 |
| 실행 시간 | 새 파일 test_issue25_plan_reuse.py 는 전체 17건이 1.5 s 안에 끝난다(브로커·herdr 의존 없음, 모두 stub) |
7. 문서 (항목 7)
- 옛 서술("기존에 승격된 계획서가 있다면 이를 로드")은
.agents·docs어디에도 남아 있지 않다(전수 grep). - SKILL.md 의 사용법 문자열·CLI 표·Workflow 예시·Mermaid 다이어그램이 모두
--plan-file기준으로 맞춰졌고, "계획서가 비어 있을 때" 절이 네 지점의 판정을 정확히 설명한다. - 러너 usage 와 셸 usage 문자열이 동일하다(P7 이 이를 고정한다).
- 배포 사본: 저장소의
.claude/skills/*는../../.agents/skills/*심링크라 자동으로 동기화된다. 사용자 홈~/.claude/skills에는 mux 스킬 사본이 없다. 옛 문구를 가진 사본은 발견되지 않았다.
8. 병합 차단 여부 (항목 8)
버전 락스텝 bump 부재(lib.sh:32·eager.py:16·loop SKILL.md 모두 4.7.0, 오케스트레이터가 4.7.1 로 처리 예정) 외에 병합을 막을 결함은 없다.
| # | 비차단 지적 | 근거 |
|---|---|---|
| E1 | 레거시 셸 러너(MAM_LOOP_PY=0)의 plan 모드 읽기에는 빈 계획서 검사가 없다: CURRENT_PLAN=$(cat "$PLAN_FILE")(run_loop.sh:501). D13/D14 계약은 파이썬 러너에만 성립하는데 SKILL.md 의 "계획서가 비어 있을 때" 절은 이를 명시하지 않는다 |
후속: 한 줄 거부 또는 문서에 "Python 러너 기준" 명시 |
| E2 | --plan-file 로 계획을 실은 실행에서 [ESCALATE: PLANNER] 는 재계획 없이 일반 수정 라운드로 내려간다(runner.py:1275 가 plan_mode 로 게이트). 샌드박스 구동으로 확인(Planner 위임 0건). 이 게이트는 PR 이전부터 있던 것이고 SKILL.md 서술과도 모순되지 않지만, "계획서를 갖고 도는 루프인데 에스컬레이션이 무력화된다"는 점은 문서에 없다 |
문서 보강 권고 |
| E3 | 리뷰 격리 워크트리에는 .venv 가 없어, run_loop.sh 를 하위 프로세스로 띄우는 스위트는 AGENT_PYTHON_BIN 없이는 반드시 실패한다(§6-1). 또한 그 워크트리에서 pytest 를 돌리면 무시되는 .mam/ 이 생긴다. PR #32 가 아니라 Issue #21 항목 5 운영 쪽 후속이다 |
리뷰 브리프의 테스트 명령에 AGENT_PYTHON_BIN=<제어 루트 .venv/bin/python> 를 포함하는 것을 권고 |
| E4 | runner.py:1070 의 os.path.getmtime(self.plan_file) 는 파싱 시점과 Phase 1 사이에 파일이 사라지면 예외로 죽는다(메시지 없는 트레이스백). 극히 좁은 경합 |
사소 |
9. 교차검증 가이드
RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260921T150212Z-97d845/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python
git -C "$RP" rev-parse HEAD HEAD^{tree} # 1e8265... / 4fd82a... = 브리프의 head·tree
# --plan-file 실패 경로 (임시 제어 루트에서만)
C=$(mktemp -d); mkdir -p "$C/ctl" "$C/elsewhere/plans"; : > "$C/ctl/empty.md"; echo x > "$C/elsewhere/plans/p.md"
(cd "$C/elsewhere" && PYTHONPATH="$RP/.agents/skills" WORKSPACE_ROOT="$C/ctl" MAM_STATE_JSON='{"herdr_sessions":[]}' \
"$PY" -m lib_py.loop --creator c --task t --plan-file plans/p.md; echo "rc=$?") # ctl 기준 경로로 실패, rc 1
# 수정 없이는 실패하는지 (사본에서만)
S=$(mktemp -d); rsync -a --exclude .venv --exclude .git --exclude .mam --exclude nats-docker "$RP/" "$S/"
for f in .agents/skills/lib_py/loop/runner.py .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
.agents/skills/multi-agent-mux-loop/SKILL.md; do git -C "$RP" show 6fc9194:"$f" > "$S/$f"; done
(cd "$S" && PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -q -p no:cacheprovider --basetemp=/tmp/bt tests/test_issue25_plan_reuse.py) # 13 failed
# 실패 2건이 환경 문제임을 확인
(cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -q -p no:cacheprovider --basetemp=/tmp/bt2 \
tests/test_loop_cli.py::test_creator_parses_and_reaches_session_check \
tests/test_issue5_phase3_loop.py::test_p3_02_run_loop_wrapper_delegates_to_python_default) # 2 passed
[VERDICT: PASS]