release(v4.7.1): bump framework and eight skills to 4.7.1 in lockstep; archive PR #32 merge-side reviews

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 (base 6fc9194 -> head 1e82651,
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
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-22 00:38:10 +09:00
1 parent 1e826515cc
commit 6b7198f73c
24 files changed
+441 -26

No files matched your search

@@ -0,0 +1,14 @@
{
"loop_id": "loop-20260921T150212Z-97d845",
"base_sha": "6fc9194f896eb2e13e123b8ebe7a9089b629764d",
"head_sha": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd",
"tree_sha": "4fd82ad1c2682d12c3f5114818f97291af28a032",
"review_commit": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd",
"reviewer": "planner-reviewer-claude-01",
"job_id": "293e905f",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-09-21T15:25:44Z"
}
@@ -0,0 +1,139 @@
# 🔍 Review Report — PR #32 병합 전 독립 리뷰 (job `293e905f`)
- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) · 2026-09-22
- **대상**: PR #32 `fix/issue-25` **@ `1e82651`**, base `main 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`)와 승격 경로(`:1367` `if 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. 교차검증 가이드
```bash
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]