Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-645adb2a.md
T
GodopuandClaude Opus 5.5 2e90c809d3 release(v4.8.1): bump framework and eight skills to 4.8.1 in lockstep; archive PR #43 reviews (two rounds)
Issue #26: one .mam.env parser for the wrapper and the Python tools,
decimal integer knobs, tests on a local MQTT broker. Merge-workspace
review: round 1 (@b58edac) claude agy PASS, codex NOT PASS (R1-R4), grok
no verdict (its late report, archived as report-c9ed5b14.md, found R1 too);
round 2 (@0eec795) 4/4 PASS. Reports and round-2 identity sidecars go to
.agents/reports/<session>/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-02 00:23:22 +09:00

14 KiB
Raw Blame History

🔍 Review Report — PR #43 fix/issue-26 리뷰 (job 645adb2a)

  • Reviewer: planner-reviewer-claude-01 (role: planner,reviewer) · 2026-10-01
  • 대상: PR #43 @ b58edac, base main e1eb22b (v4.8.0). 6커밋, 17파일 +1590/−94, git diff --check 클린.
  • 검증 경로: /var/folders/.../T/mam-review/loop-20261001T131705Z-a60092/1. identity 확인: HEAD = b58edace…4893, HEAD^{tree} = 14b670fb…6ffa — 브리프와 일치.
  • 안전 규칙 준수: 검증 경로·소스 워크트리·본 체크아웃(main e1eb22b) 모두 수정·커밋·checkout 없음. 실제 .mam.env 는 값을 출력하지 않고 형태만 셌다($·export·인라인 #·따옴표 줄 수 = 모두 0). 파서 실험은 scratchpad 의 합성 파일로만. 테스트는 브리프대로 .mam.env 를 export 하지 않고(MQTT_* 미설정 확인) 돌렸다. 실제 .mam·데몬·세션·herdr 조작 없음, push·merge·PR 상태 변경 없음.
  • 참고(이 잡의 liveness): 이 잡의 started 발행 때 브로커 100.69.147.175:1883 이 연결을 거부했다(Connection refused, 지금도 포트 닫힘). publish_event 는 rc 2 였지만 레코드는 running·events.log 에 started(토큰 포함)가 기록됐고 heartbeat 타이머도 떠 있다 — #18 계약상 로컬 증거로 충분하다. 브로커 쪽은 오케스트레이터 확인이 필요하다(아래 판정과 무관).

결론 요약

병합을 막을 결함은 없다. 두 로더는 같은 입력에서 같은 결과를 내고(합성 파일 15줄 실측), 래퍼의 eval 경로는 어떤 값도 셸 해석하지 않으며(명령 치환 주입 실험: 실행 0), bash mam_int 와 Python _positive_int 는 15가지 입력에서 결과가 같다. 로컬 브로커로 옮긴 시험들은 skip 을 fail 로 바꾸고 양쪽 로그·감사 기록까지 단언해 오히려 강해졌다. 비차단 관찰 5건(R1–R5).

1. 파서 규칙 (검토 항목 1)

envfile.py:45-73 parse, mqtt_common.py:98-101 은 envfile.apply 에 위임 — Python 쪽 로더는 하나다. 래퍼는 같은 파일을 --shell 로 실행한다. 같은 합성 파일을 두 경로에 넣은 결과:

입력 줄 Python 로더 래퍼 eval 후
BOMKEY=1 (UTF-8 BOM) 건너뜀(invalid key, 1행 경고) 미설정
A = spaced \r (CRLF) 'spaced' spaced
B="q's" "q's" 동일
C='x'' "x'"(바깥 한 쌍만) 동일
D=a=b=c 'a=b=c' 동일
E= '' ''
export F=exp 건너뜀(invalid key) 미설정
G=val # trailing 'val # trailing' 동일
H=$(touch …/PWNED) \id` ${HOME}` 글자 그대로 글자 그대로, PWNED 파일 생성 안 됨
9BAD=x / noequals 건너뜀 + 경고 미설정
IFS=x / BASH_ENV=x 건너뜀(reserved) 미설정
I="unbalanced '"unbalanced' 동일
J= "inner" 'inner' 동일

두 경로가 완전히 일치한다. 경고에는 값이 찍히지 않는다(줄 번호·사유만). 이전 Python 로더(strip('"').strip("'"))는 따옴표를 여러 겹 벗기고 키 검사가 없었는데, 이제 한 쌍만 벗기고 형식 오류를 경고한다 — 문서화된 규칙과 맞다.

R1 (비차단, 문서): .mam.env.example 은 $HOME/... 예시를 /home/you/... 로 바꿔 "셸 해석 없음" 과 일관되게 했지만, 머리말에 규칙 자체(셸 확장 없음, 인라인 # 은 값의 일부, export 접두어 불가, BOM 저장 시 첫 줄 무시, 이미 있는 환경변수가 우선)를 적지 않았다. 특히 래퍼 사용자에게는 세 가지가 동작 변경이다 — 전에는 bash source 가 export KEY=·$HOME 확장·인라인 주석을 받아들였다. 실제 .mam.env 에는 해당 줄이 없음을 확인했다(형태만 셈).

2. 래퍼의 eval 경로 (검토 항목 2)

multi-agent-mux-delegate-job:41-46: python -I envfile.py --shell 의 출력을 eval. 출력은 envfile.py:97-98 의 print(f"export {key}={shlex.quote(val)}") 뿐이고, key 는 _KEY.fullmatch 를 통과한 것만, val 은 splitlines 로 개행이 없고 shlex.quote 로 감싸진다 — 주입 실험에서 $()·백틱·${} 모두 글자 그대로 남았다.

  • 래퍼가 그 전에 세운 비-export 변수는 SCRIPT_DIR·REPO_ROOT·TARGET_ENV 셋뿐이고, 셋 다 환경으로 넘겨 파일이 덮어쓸 수 없다(:42). env-wins 실측: D=fromenv 가 파일의 D=a=b=c 를 이긴다.
  • 실패 경로: 파일이 없으면 rc 0·출력 없음, 읽을 수 없으면 stderr WARNING: … cannot read (PermissionError) 후 rc 0 — 래퍼는 설정 없이 계속한다. 래퍼의 || { echo "ERROR: cannot read $TARGET_ENV"; exit 1; }(:44)는 Python 실행 자체가 실패할 때(인터프리터 없음 등)만 걸려, 메시지와 실제 원인이 어긋난다(R2, 사소).
  • -I: PYTHONPATH·사용자 site 를 무시하지만 envfile.py 는 표준 라이브러리만 쓰고 파일 경로로 실행되므로 문제없다. 오히려 .mam.env 를 읽기 전 환경의 PYTHON* 이 파서에 끼어들 여지를 없앤다.

3. 우선순위 변경의 영향 (검토 항목 3)

전에는 래퍼의 source 가 환경을 덮어썼고 Python 로더는 환경을 지켰다 — 같은 파일이 두 경로에서 다른 결과를 냈다. 이제 둘 다 env-wins.

  • 루프 러너가 래퍼에 넘기는 env(DELEGATE_JOB_PYTHON, --registry-dir 대신 쓰는 MAM_JOBS_DIR 등): 전에는 파일 값에 덮일 수 있었다 → 이제 러너 값이 이긴다. 개선.
  • 워크트리 creator: create/resume 이 넘기는 절대경로 바인딩(AGENT_SESSIONS_YAML·MAM_JOBS_DIR·…)이 이제 파일 값에 덮이지 않는다 — #21 의 의도와 맞는 개선.
  • 원격 러너(remote.py)는 원래 Python 로더라 변화 없음. 테스트는 conftest 가 MAM_ENV_FILE 을 없는 파일로 고정해 무관.
  • 동작이 나빠지는 경로: 셸에 오래된 MQTT_* 를 export 해 둔 채 파일만 고친 사용자는 이제 파일 값이 적용되지 않는다. 이 부분이 R1 의 문서 누락과 겹친다.

4. 정수 정규화 (검토 항목 4)

bash mam_int(lib.sh:2345-2356)와 Python _positive_int(runner.py:71-82)를 같은 15개 입력에 돌린 결과 전부 일치: ' 30 '→30, 0700→700(8진 아님), 0900→900, 0→거부(min 1), -5·+5·1e3·30s·전각 30·빈 값 → 거부, 999999999→수용, 1000000000→거부(9자리 상한), 0000000000000000001→1(앞 0 제거 후 길이 판정).

  • 래퍼: 환경 노브는 mam_int_knob(잘못되면 경고 + 기본값, :154-156), 플래그는 parse_opts 끝의 루프(잘못되면 ERROR … not a positive decimal integer rc 1, :196-200). MAM_INJECT_ACK_GRACE_S 는 min 0(0 = 대기 없음 허용, 의미상 맞다), ACK_TIMEOUT·heartbeat·budget 은 min 1. lib.sh SKS_* 대기 노브는 min 0(대기 생략 허용), delegate_wait_started 도 0 허용(:2379).
  • 빠진 노브: $(( )) 안에 환경 기본값을 직접 쓰는 자리는 남아 있지 않다(B5 정적 검사가 고정). [ -ge ]/[ -le ]/sleep 에만 쓰이는 노브(MAM_READY_EMPTY_GIVEUP·SKS_EMPTY_GIVEUP·SKS_IDLE_RETRIES·…)는 test 의 비교가 원래 10진수라 8진 문제가 없고, PR 이 표로 분류해 "모든 정수 노브가 분류됨" 을 시험한다(test_b5_every_integer_env_default_is_classified). 그 노브들에 숫자가 아닌 값이 오면 [ 가 오류(rc 2)를 내는 기존 동작은 남는다(R3, 범위 밖).

5. 로컬 브로커와 이전된 시험 (검토 항목 5)

tests/_local_mqtt.py 는 MQTT 3.1.1 의 필요한 부분을 클라이언트가 기대하는 순서로 처리한다: CONNECT(레벨 3/4만, 그 외 CONNACK rc 1)→CONNACK, SUBSCRIBE→SUBACK(QoS 최대 1 부여) 뒤 매칭되는 retained 전달, PUBLISH QoS1 은 라우팅·retained 저장 뒤에 PUBACK(b58edac — 발행자가 ack 를 받은 시점에 브로커 상태가 이미 반영됨), 빈 payload retained 는 삭제, UNSUBSCRIBE→UNSUBACK, PINGREQ→PINGRESP, +/# 와일드카드와 $ 토픽 예외(topic_matches). 미지원(QoS2·will·지속 세션)은 이 저장소가 쓰지 않는 기능이다. 127.0.0.1:0 에 바인딩해 포트 충돌이 없고, 스레드는 daemon 이며 stop() 이 리스너·연결을 닫는다.

이전된 시험의 단언은 약해지지 않았고 강해졌다:

  • test_acceptance5: 공개 브로커용 _skip_if_broker_did_not_deliver·no CONNACK skip 이 삭제되고, 대신 러너 로그의 DISPATCH·published started/completed·DONE rc=0 과 오케스트레이터 감사 기록의 dispatch_verified·dispatch_fingerprint == sha256(token) 을 양쪽에서 단언한다.
  • test_issue19: "브로커 도달 불가면 skip" 이 pytest.fail 로 바뀌었다(로컬 브로커는 항상 떠 있으므로).
  • S9/S10 의 _live_broker()(2·3회차 리뷰에서 내가 P2 로 지적한 공개 브로커 의존)가 local_broker_env() 로 대체됐다.
  • test_b19 는 send_keys_safe 본문을 앞 2500자가 아니라 함수 끝까지 검사하게 고쳐졌다(정확성 개선).
  • 정적 검사(test_local_mqtt.py:215)가 공개 브로커 주소의 재유입을 막는다. 시험 로그에 hivemq·실제 브로커 IP 흔적 0건.

6. 테스트 실행 (검증 방법·검토 항목 6)

cd <검증 경로> && AGENT_PYTHON_BIN=<pr-43 .venv> PYTHONDONTWRITEBYTECODE=1 <pr-43 .venv> -m pytest -p no:cacheprovider -q -rfEs \
  --basetemp=<밖> tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_local_mqtt.py \
  tests/test_issue18_liveness_contract.py tests/test_issue33_budget.py tests/test_issue30_stall_watch.py tests/test_tier1_unit.py
(.mam.env 미export, MQTT_* 미설정)
→ 228 passed, 1 skipped in 525.44s (0:08:45)   failed 0   (skip = S6, /proc)
  • PR #41 리뷰 내내 공개 브로커 때문에 흔들리던 S9·S10·R2·B3·acceptance 계열이 외부 브로커 없이 전부 통과했다 — PR 의 주장 확인.
  • 기준 실패 주장(검토 항목 6): tests/test_issue10_13_purge.py 를 이 PR 트리와 base(e1eb22b 를 git archive 로 풀어 놓은 사본)에서 각각 단독 실행한 결과 둘 다 23 passed, 실패 0. 즉 이 호스트에서 이 리뷰어의 조건으로는 "12건 기준 실패" 가 재현되지 않는다. 양쪽 결과가 같으므로 어느 경우든 PR 회귀는 아니다. 오케스트레이터의 전체 스위트에서만 나타난다면 시험 간 순서·상태(예: 실제 홈의 대화 파일, 이전 시험이 남긴 환경) 의존을 의심할 만하다(R4).
  • 격리: 실제 .mam/jobs 는 이 잡 자신의 파일 외 추가 0. 검증 경로 tracked 파일 변화 없음(HEAD b58edac), 본 체크아웃 main e1eb22b. 실행 뒤 호스트에 남은 stall_watch(이 잡을 감시하는 오케스트레이터의 것)·sleep 300(다른 세션의 프로세스 그룹) 은 내 실행과 무관함을 부모/프로세스 그룹으로 확인했다.

7. 설계 경계 (검토 항목 7)

  • 판정이 Python 으로 이동: .mam.env 해석(키 검증·예약 변수 거부·우선순위)이 bash source 에서 envfile.py 로 옮겨졌다 — #17 항목 4 의 ".mam.env 파싱(#26) — bash source 제거" 그대로다. 래퍼에는 실행과 eval 만 남는다.
  • bash 에 새로 생긴 로직: mam_int/mam_int_knob 과 parse_opts 의 플래그 검증. 이는 bash 산술로 들어가는 값을 그 자리에서 정규화하는 입력 정제이고, Python 쌍둥이(_positive_int)와 한 규칙으로 고정됐다(B1 시험). 판정(무엇을 할지)이 아니라 형식 변환이라 경계 위반으로 보지 않지만, 1회차 PR #41 리뷰의 M4(MAM_JOB_TIMEOUT_S 검증이 bash 에 있음)와 같은 부류이므로 항목 4 에서 래퍼 인자 처리가 Python 으로 갈 때 함께 옮길 목록에 둘 것을 권한다(R5).

8. 판정

버전 락스텝 bump 부재는 판정에서 제외한다. 병합을 막을 결함은 없다.

# 비차단 관찰 근거
R1 .mam.env.example 머리말에 새 파싱 규칙과 env-wins 가 적혀 있지 않다 — 래퍼 사용자에게 export 접두어·$ 확장·인라인 주석·우선순위 4가지가 동작 변경 .mam.env.example:1-20; §1·§3
R2 래퍼의 ERROR: cannot read $TARGET_ENV 는 Python 실행 실패에만 걸리고, 실제 읽기 실패는 경고 후 설정 없이 진행 — 메시지가 원인과 어긋난다 multi-agent-mux-delegate-job:44, envfile.py:52-53
R3 [ -ge ]/sleep 전용 정수 노브에 숫자가 아닌 값이 오면 [ 오류(rc 2)를 내는 기존 동작은 남는다 PR 이 분류표로 의도적으로 범위 밖 처리
R4 "test_issue10_13_purge.py 12건 기준 실패" 가 이 리뷰어 조건(단독, PR·base 양쪽)에서는 재현되지 않음 — 전체 스위트 순서 의존 가능성 §6
R5 mam_int/플래그 검증은 bash 의 입력 정제 — 항목 4 이전 목록에 추가 권고 lib.sh:2345-2364, 래퍼 :196-200

9. 교차검증 가이드

RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}'                         # b58edac… / 14b670f…
# 합성 .env 15줄을 envfile.apply 와 `-I envfile.py --shell` + eval 양쪽에 → 결과 동일, $(…)/`…` 미실행 (§1 표)
# mam_int vs _positive_int 15개 입력 → 전부 동일 (§4)
# base: git -C "$RP" archive e1eb22b | tar -x -C <tmp> → tests/test_issue10_13_purge.py → 23 passed (PR 도 23 passed)

[VERDICT: PASS]