Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-f1174fc0.md
T
GodopuandClaude Fable 5.1 cf5d1142e9 release(v4.7.0): bump framework and eight skills to 4.7.0 in lockstep; archive PR #29 reviews
Issue #21 items 4, 5, 7 (worktree helpers, review identity, acceptance
A1-A8), #20 test isolation, #27 .gitignore managed block, #28 host
portability. VERSIONS.md carries the entry; the eight review reports of
PR #29 (two rounds plus the codex re-review) are archived under
.agents/reports/<session>/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-09-20 21:43:25 +09:00

190 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔍 Review Report — PR #29 델타 리뷰 2회차 (job `f1174fc0`)
- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) · 2026-09-20
- **대상 델타**: `46ea4cc..08db1da` (3커밋, `git diff --stat` = **16 files, +159/−34**), base `main 1134d74` (v4.6.0).
- **검토 트리**: 워크트리 `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-29` (detached @ `08db1da`).
- **1회차 내 판정**: PASS (job `291e38b5`). 이번에는 **델타가 그 판정을 뒤집는지**를 본다.
- **안전 규칙 준수**: 워크트리 읽기 전용(수정·커밋·checkout 없음, `git status --short` 클린 유지). 본 체크아웃 `main @ 1134d74` 그대로. `worktree_prepare.sh`·`worktree_remove.sh` 를 실제 워크트리·본 체크아웃에 실행하지 않았다 — 모든 실행은 scratchpad 의 임시 복사본/임시 저장소(uuid4 이름)에서만 했다. 실제 `.mam`·데몬·세션 조작 없음, push·merge·PR 상태 변경 없음, purge·scrub 실행 없음.
## 결론 요약
**R1·R2·R3 은 실제로 닫혔다.** 특히 R1/R2 는 (a) 결함을 임시 저장소에서 직접 재현했고, (b) 워크트리 복사본에서 수정만 되돌렸을 때 새 회귀 4건이 정확히 실패하는 것까지 확인했다. 1회차 PASS 를 뒤집을 사유는 없다. 비차단 4건(D1–D4)만 남긴다. 그중 D1 은 이번 델타가 새로 만든 작은 구멍이다.
## 1. R1/R2 — `--untracked-files=all` (항목 1)
### 1-1. 결함 재현 (임시 저장소)
```
git config status.showUntrackedFiles no ; echo secret > untracked.txt
git status --porcelain --ignore-submodules=none → (빈 출력) ← 옛 검사: "클린"
git status --porcelain --untracked-files=all --ignore-submodules=none → ?? untracked.txt ← 수정본
```
표시 설정 하나로 "강제 제거해도 되는가"라는 판정이 뒤집혔다는 codex 의 지적은 사실이다.
### 1-2. 세 검사 모두에 들어갔는가 — 그렇다
| 파일:라인 | 검사 |
|---|---|
| `.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh:93` | `judge_tree()` 의 워크트리 청결 검사 |
| `.agents/skills/lib_py/worktree.py:170` | `submodule_findings()` 의 `dirty` 검사(`modules/` 아래 모든 저장소) |
| `.agents/skills/lib_py/loop/runner.py:718` | `_dirty_submodules()` (리뷰 스냅샷 승인 전 서브모듈 검사) |
스킬 트리 전체에서 `status --porcelain` 을 부르는 나머지 두 곳도 확인했다.
- `worktree_prepare.sh:209`: 롤백 후 "이 워크트리가 아직 쓸 수 있는 체크아웃인가"만 보는 용도(stdout 은 버리고 stderr 만 본다). 파괴 판정이 아니다 — 플래그 불필요.
- `runner.py:581`: 프롬프트의 `미커밋 변경: 있음/없음` **표시용**. 판정에 쓰이지 않는다(→ D3 참조).
### 1-3. 소스 트리 스냅샷은 별도 조치가 필요 없는가 — 필요 없다
`_worktree_tree()` 의 `git add -A` 는 `status.showUntrackedFiles` 를 **보지 않는다**. 임시 저장소에서 `showUntrackedFiles=no` 로 설정한 뒤 `read-tree HEAD` → `add -A` → `write-tree` 한 트리에 `untracked.txt` 가 그대로 들어갔다. 즉 스냅샷은 이미 untracked 를 고정하고 있었고, 구멍은 "서브모듈 검사"와 "제거 판정"에만 있었다. 수정 범위가 정확하다.
`_change_set_state()`(`runner.py`)와 `diff_collect.sh` 의 untracked 수집은 `git ls-files -o --exclude-standard` 라 이 설정의 영향을 받지 않는다. 빈 변경분 fail-closed 가드도 안전하다.
### 1-4. 새 회귀가 수정 없이는 실패하는가 — 확인함
워크트리를 `.git`·`.venv` 제외하고 scratchpad 에 복사한 뒤, **세 곳의 `--untracked-files=all` 만 제거**하고 돌렸다(워크트리 자체는 건드리지 않았다).
```
4 failed, 5 passed in 14.69s
FAILED test_issue21_phase2_loop_a.py::test_w6e2_untracked_submodule_file_hidden_by_config_blocks_the_forced_removal
FAILED test_issue21_phase2_loop_a.py::test_w6e_work_written_while_stopping_is_not_force_removed[True]
FAILED test_issue21_loop_b.py::test_l17_dirty_submodule_is_refused_or_makes_the_round_stale[untracked-hidden]
FAILED test_issue21_loop_b.py::test_l17_dirty_submodule_is_refused_or_makes_the_round_stale[during-hidden]
```
기존 변형(`[False]`, `before`, `before-ignored`, `untracked`, `during`)은 그대로 통과했다. 즉 새 회귀는 **정확히 이 수정만** 검증하며, 회귀 방지력이 실재한다. `test_w6e2` 가 단언 안에서 `assert _git(wt,"status","--porcelain","--ignore-submodules=none").stdout == "", "git hides it by default"` 로 전제 자체를 고정한 것도 좋다.
## 2. R3 — 워크트리 자체 venv (항목 2)
`worktree_prepare.sh:111-134` 의 분기를 읽고 문서·테스트와 대조했다(스크립트는 실제 트리에 실행하지 않았다).
| 요구 | 구현 | 판정 |
|---|---|---|
| 심링크가 아닌 실제 venv | `"$MAIN/.venv/bin/python" -m venv "$WT/.venv"` + `requirements.txt`·`requirements-dev.txt` 설치 | **충족**. 1회차 내 지적 N2 가 닫혔다 |
| 멱등성 | `[ -x "$WT/.venv/bin/python" ]` → `VENV_STATE="kept"` | 충족(`test_w1` 이 `VENV=created` → `VENV=kept` 순서를 고정) |
| 남은 링크 거부 | `[ -L "$WT/.venv" ]` → exit 1, **아무것도 바꾸지 않음** | 충족(`test_w1b` 가 링크 보존까지 단언) |
| 인터프리터 부재 | `WARNING: ... not found; no venv created` + `VENV=skipped` | 충족 |
| 설치 실패 시 exit 1, 워크트리 유지 | `pip install` 실패 → 메시지 `(the worktree is kept)` + exit 1. `git worktree add` 는 이미 끝난 뒤라 실제로 남는다 | 충족 |
| 문서 정합 | `create/SKILL.md:246`, `stop/SKILL.md:123`, 설계 문서 4장이 모두 "자체 venv·링크 거부·`VENV=` 요약"으로 갱신됨 | 충족 |
| ignore 규칙 정합 | `.gitignore` 관리 블록의 `/.venv`(#27) 와 공유 `info/exclude` 의 `/.venv` 는 **디렉터리에도 그대로 적용**된다. `test_w1` 이 준비 후 `status --porcelain` 이 비는지 단언 | 충족 |
`bash -n` 은 변경된 두 스크립트 모두 통과한다. `req=()` 배열 + `[ -f ... ] && req+=(...)` 가 `set -euo pipefail` 아래서 조기 종료하지 않는지 macOS `/bin/bash` 3.2.57 로 직접 확인했다(두 파일 모두 없을 때도 루프 끝까지 도달, rc 0).
### D1 (비차단, 이번 델타가 새로 만든 구멍) — 설치 실패 뒤 재실행이 조용히 `VENV=kept`
`pip install` 이 실패하면 exit 1 이지만 `$WT/.venv/bin/python` 은 **이미 만들어져 있다**(`:121` 에서 venv 생성이 먼저 끝난다). 그래서 사용자가 그대로 재실행하면 `:117-118` 의 `-x` 검사에 걸려 `VENV=kept` 로 넘어가고, **요구사항이 설치되지 않은 환경**을 정상으로 보고한다. 메시지는 "fix it and re-run" 이라 재실행을 유도한다.
→ 권고: 설치 성공을 표시하는 마커(예: `$WT/.venv/.mam-requirements-ok`)를 두고 `kept` 판정에 함께 쓰거나, `kept` 경로에서도 `requirements*.txt` 설치를 한 번 더 시도.
### D2 (비차단, 정보) — 준비 비용·네트워크 의존
이제 워크트리마다 venv 생성 + PyPI 설치가 필요하다. 오프라인이거나 PyPI 가 막힌 호스트에서는 prepare 가 exit 1 로 끝난다(이전에는 링크 한 번이라 즉시 끝났다). SKILL.md 에 설치 동작은 적혀 있으나 "네트워크가 필요하다"는 명시는 없다.
### D3 (비차단, 사소) — 프롬프트의 `미커밋 변경` 표시는 여전히 설정에 취약
`runner.py:581` 의 `git status --porcelain` 에는 플래그가 없어, `status.showUntrackedFiles=no` 인 소스에서는 untracked 만 있을 때 `미커밋 변경: 없음` 으로 표시된다. 판정·스냅샷·서브모듈 검사는 모두 안전하므로 **표시 오차**일 뿐이지만, R1 의 취지("표시 설정이 판단 재료를 바꾸면 안 된다")를 생각하면 같은 플래그를 주는 편이 일관적이다.
## 3. #28 이식성 수정(`7a94ba2`)이 의도를 약화시키는가 (항목 3)
### 3-1. `ps -eo pid=,ppid=,args=` — 양쪽에서 같은 출력인가
이 호스트(macOS 15, arm64)에서 **두 형태를 같은 프로세스에 대해 직접 비교**했다(파이프로 캡처, 코드와 동일 조건).
```
['-axo','pid=,ppid=,command='] → 길이 575, 인자 X 400개 그대로
['-eo','pid=,ppid=,args='] → 길이 575, 인자 X 400개 그대로
```
- **구분자·필드**: 동일(`pid ppid args`), 잘림 없음. 400자 argv 가 온전히 나왔다 — 파이프 출력일 때는 폭 제한이 걸리지 않는다.
- **이식성**: `-e`(모든 프로세스), `-o`(포맷), `args`(전체 명령행)는 POSIX 다. 반대로 `-a`+`-x` 조합과 `command=` 는 BSD 관용이고 procps 에서는 경고·다른 해석이 나올 수 있다. purge 12건의 원인 가설로 타당하며, **macOS 동작은 그대로**다(위 실측).
- 남는 차이: Linux 의 `args` 도 커널 `/proc/<pid>/cmdline` 기준이라 매우 긴 argv 는 환경에 따라 잘릴 수 있다. 다만 이 코드의 판정(에이전트 CLI 여부, UUID 포함 여부)은 앞쪽 토큰에 의존하므로 실질 영향은 없다.
### 3-2. `_normalize_help`
`optional arguments:` → `options:` 치환 + 공백 런 축약이다. 즉 **레이아웃(줄바꿈·들여쓰기) 동등성은 포기하고 내용 동등성만** 본다. 골든 캡처가 3.9 에서 떠졌고 대상 호스트가 3.14 라는 사정상 타당하며, 플래그·인자·설명 문구가 바뀌면 여전히 실패한다(시험의 목적은 "shim 이 구현과 CLI 상 구분 불가"이므로 유지). 약화는 제한적이고 대안(버전별 골든 캡처)은 유지비가 크다 — 수용.
### 3-3. `test_workspace_scope.py` 의 `bash -c`
`shell=True` 는 `/bin/sh` 를 쓰므로 dash 에서 `source` 가 없다. 문자열을 그대로 `["bash","-c", cmd]` 로 옮긴 것은 최소 변경이며 검증 내용(=`resolve_herdr_session` 의 출력)은 그대로다. 수용.
### 3-4. conftest 의 `MAM_ENV_FILE` 고정
`tests/conftest.py:173-177` 이 autouse 픽스처에서 `MAM_ENV_FILE` 을 `tmp_path/.mam-test-state/.mam.env.absent` 로 고정한다. 방향은 #20·#15 와 같은 **격리 강화**이고, 실제 `.mam.env` 를 읽어야 하는 시험은 스스로 `MAM_ENV_FILE` 을 설정한다(`test_issue14/18/5_phase1/21_worktree_prereqs/20_state_isolation` 모두 자체 설정을 갖고 있다). "브로커 없음" 계열 시험이 호스트의 실 설정을 주워 읽던 것이 오히려 결함이었으므로 계약을 약화시키지 않는다.
## 4. `90a289d` 벽시계 여유 8 → 15 s (항목 4)
이 시험이 고정하는 결함은 "idle grace 가 wall clock 을 **무시**해 `2 × TIMEOUT`(≈40 s) 에 닫히는 것"이다. 임계는 `20 + 15 = 35 s` 이므로 결함 시나리오(40 s)는 여전히 실패한다. 정상 종료는 내가 1회차 전체 스위트에서 실측한 값이 28.49 s·28.50 s(단독 재실행 포함 2회 일치)였다.
```
정상 28~29 s < 새 임계 35 s < 결함 40 s
```
여유는 5 s 로 좁아졌지만 계약(“wall clock 을 넘기지 않는다”)의 판별력은 남는다. 1회차에서 내가 유일한 비-flake 실패로 보고했던 항목이 이 커밋으로 해소된다. 다만 `timeout_s` 가 더 커지면 `2 × TIMEOUT` 과 `TIMEOUT + 15` 의 간격은 벌어지고 작아지면 좁아지므로, **고정 여유(+15)보다 비율(예: `timeout_s * 1.5`)** 이 계약을 더 정확히 표현한다(D4, 비차단 권고).
## 5. 테스트 실행 (워크트리, 이 리뷰어가 직접)
```
cd <pr-29 워크트리> && PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -p no:cacheprovider -q -rfEs \
tests/test_issue21_phase2_loop_a.py tests/test_issue21_loop_b.py tests/test_issue21_worktree_prereqs.py \
tests/test_issue21_acceptance.py tests/test_workspace_scope.py tests/test_issue5_phase1_delegation.py \
tests/test_deploy_freshness.py tests/test_version_consistency.py
→ 208 passed in 322.94s (failed 0 · errors 0 · skipped 0)
```
- 브리프가 지적한 **`tests/test_issue28_*.py` 는 이 트리에 없다**(#28 수정은 기존 파일에 들어갔다) — 규칙대로 건너뛰었다.
- **skip 0 건**: `test_deploy_freshness.py` 의 새 `skipif`(D-11~D-19, D-22~D-30)는 이 호스트에서 **전부 실행**됐다(서브모듈 자산이 있으므로 의도대로다). 브리프 항목 3 의 우려("로컬 자산이 있는 호스트에서는 실행되어야 한다")는 실측으로 해소.
- 가장 느린 시험 10건은 모두 herdr mock·정지/재개 경로이며 브로커 타임아웃 계열 실패는 없었다.
### 격리·위생 실측
| 항목 | 결과 |
|---|---|
| 실제 `.mam/jobs` 전후 | **차이 0** |
| 워크트리 `git status --short --ignored` 전후 | **완전 동일** |
| 워크트리 HEAD / 본 체크아웃 | `08db1da` / `main 1134d74` (둘 다 변화 없음) |
| 샌드박스 실험 | scratchpad 임시 디렉터리에서만(복사본 리버트 실행 포함). 실제 스크립트·워크트리에 prepare/remove 실행 없음 |
## 6. 병합 차단 여부 (항목 5)
버전 락스텝 bump 부재(`lib.sh:32`·`eager.py:16` 모두 `4.6.0`, 오케스트레이터가 4.7.0 으로 처리 예정) 외에 **병합을 막을 결함은 없다**. 1회차 PASS 판정은 이번 델타로 뒤집히지 않으며, 1회차에 내가 남긴 비차단 지적 중 **N2(.venv 심링크)는 이 델타로 해소**됐다. 1회차의 N1(스냅샷 커밋이 ref 없이 남아 gc 로 소멸)은 그대로 남아 있으니 후속 이슈로 유지하기를 권한다.
| # | 이번 회차 지적 | 성격 |
|---|---|---|
| D1 | pip 설치 실패 후 재실행이 `VENV=kept` 로 조용히 통과 | 후속 권고(이번 델타가 만든 것) |
| D2 | prepare 가 워크트리마다 venv 생성 + PyPI 설치를 요구(오프라인 exit 1) | 정보·문서화 |
| D3 | `runner.py:581` 의 표시용 status 에는 `--untracked-files=all` 이 없음 | 사소(일관성) |
| D4 | 벽시계 여유를 고정값(+15 s)이 아니라 비율로 두는 편이 계약에 충실 | 사소 |
## 7. 교차검증 가이드
```bash
WT=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-29
# R1 결함 재현 (임시 저장소에서만)
R=$(mktemp -d); cd "$R"; git init -q .; git config user.email t@t; git config user.name t
echo a>a; git add a; git commit -qm base; git config status.showUntrackedFiles no; echo secret > untracked.txt
git status --porcelain --ignore-submodules=none # (빈 출력)
git status --porcelain --untracked-files=all --ignore-submodules=none # ?? untracked.txt
GIT_INDEX_FILE=$R/idx git read-tree HEAD; GIT_INDEX_FILE=$R/idx git add -A
git ls-tree -r --name-only "$(GIT_INDEX_FILE=$R/idx git write-tree)" # untracked.txt 가 들어 있다(= add -A 는 설정 무시)
# 수정 없이는 회귀가 실패하는지 (워크트리 복사본에서만)
S=$(mktemp -d); rsync -a --exclude .venv --exclude .git --exclude .mam "$WT/" "$S/"
sed -i '' 's/ --untracked-files=all --ignore-submodules=none/ --ignore-submodules=none/' \
"$S/.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh" "$S/.agents/skills/lib_py/loop/runner.py"
sed -i '' 's/"git status --porcelain --untracked-files=all"/"git status --porcelain"/' "$S/.agents/skills/lib_py/worktree.py"
(cd "$S" && PYTHONDONTWRITEBYTECODE=1 "$WT/.venv/bin/python" -m pytest -p no:cacheprovider -q \
tests/test_issue21_phase2_loop_a.py::test_w6e2_untracked_submodule_file_hidden_by_config_blocks_the_forced_removal \
tests/test_issue21_phase2_loop_a.py::test_w6e_work_written_while_stopping_is_not_force_removed \
tests/test_issue21_loop_b.py::test_l17_dirty_submodule_is_refused_or_makes_the_round_stale) # 4 failed, 5 passed
# ps 형태 비교(잘림·구분자)
python3 - <<'PY'
import subprocess,sys,time
p=subprocess.Popen([sys.executable,"-c","import time;time.sleep(6)","X"*400]); time.sleep(1)
for f in (["-axo","pid=,ppid=,command="],["-eo","pid=,ppid=,args="]):
l=[x for x in subprocess.run(["ps",*f],capture_output=True,text=True).stdout.splitlines() if x.split()[:1]==[str(p.pid)]]
print(f, len(l[0]), l[0].count("X"))
p.wait()
PY
```
[VERDICT: PASS]