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

16 KiB
Raw Blame History

🔍 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-11D-19, D-22D-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. 교차검증 가이드

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]