Files
multi-agent-mux/.agents/reports/planner-reviewer-codex-01/report-3001f1d3.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

15 KiB
Raw Blame History

PR #43 review — job 3001f1d3

판정: NOT PASS. 로컬 MQTT 전환의 검증은 강화됐지만, 설정 파일 읽기 실패 처리와 두 정수 정규화 구현의 동작에 재현 가능한 차이가 남아 있다. 아래 수정은 현재 설계 안에서 가능하므로 재계획 에스컬레이션은 요청하지 않는다.

검증 identity와 범위

  • loop: loop-20261001T131705Z-a60092
  • base: e1eb22bd59ce825e3a22e86433c30f7ffd833810
  • head: b58edace115205500c1197d9ce59cb5c6d724893
  • tree: 14b670fbfc82dc9179cab43cf53eef920a3d6ffa
  • 검증 cwd: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1
  • 인터프리터: /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python

검증 트리의 HEAD/tree와 clean 상태를 확인했다. 프로젝트 코드, 브랜치, 실제 세션은 변경하지 않았다. 실제 .mam.env를 읽거나 export하지 않았다. 테스트 및 탐침의 설정·레지스트리·HOME·산출물은 임시 디렉터리를 사용했다. 기준 비교는 git archive <base>로 만든 /tmp/3001f1d3-base 사본의 purge 테스트만 같은 인터프리터와 검증 cwd에서 실행했다.

수정 필요 사항

R1 — [P2] 설정 파일을 읽을 수 없는데 래퍼가 성공으로 계속 진행한다

근거: .agents/skills/lib_py/delegation/envfile.py:52는 OSError와 UnicodeDecodeError를 빈 설정과 경고로 바꾸고, :94에서 이를 받은 CLI는 :99에서 항상 0을 반환한다. 래퍼 .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:42의 실패 검사는 따라서 이 오류를 잡지 못한다.

재현: 임시 파일에 PROBE_A=one을 쓰고 chmod 000한다. 해당 파일을 MAM_ENV_FILE로 지정하고 실제 래퍼의 --help를 실행한다. HOME/WORKSPACE_ROOT와 로그·레지스트리도 임시 경로로 지정한다. 확인 후 파일 권한을 복원한다.

기대: 파일 전체 읽기 실패는 명시적 오류와 nonzero 종료. 잘못된 개별 줄을 경고 후 건너뛰는 규칙과 구분해야 한다.

실제: head는 cannot read (PermissionError) 경고와 함께 rc 0, usage 출력을 반환했다. 같은 파일과 환경의 base 래퍼는 rc 1 / Permission denied였다. 잘못된 UTF-8 파일도 head에서 경고와 rc 0이었다. 실제 submit에서는 필요한 설정 없이 기본 설정으로 진행할 수 있다. 기존 test_a1_an_unreadable_parser_fails_loudly는 이름과 달리 파일 읽기 실패가 아니라 Python 실행파일 부재만 시험한다(tests/test_issue26_envfile.py:138).

수정 방향: 공통 파서에서 전체 읽기/디코딩 실패를 개별 줄 경고와 별도로 표현하고 CLI 및 Python 진입점으로 실패를 전달한다. 기존의 선택적 파일 부재 처리와 개별 잘못된 줄 건너뛰기는 유지한다. 실제 권한 오류 및 invalid UTF-8에 대한 wrapper rc 회귀 검증을 추가한다.

R2 — [P2] 로케일에 따라 Bash와 Python이 같은 정수 입력을 다르게 해석한다

근거: .agents/skills/lib.sh:2346은 로케일 의존 [[:space:]]를 사용한다. .agents/skills/lib_py/loop/runner.py:74는 ASCII 공백 6개만 제거하고 :75에서 ASCII 숫자만 허용한다. runner.py:483은 이 결과를 명시적인 --timeout으로 전달한다.

재현: LC_ALL=en_US.UTF-8, MAM_JOB_TIMEOUT_S='\u200330\u2003'를 실제 U+2003 문자로 설정한다. 임시 HOME/WORKSPACE_ROOT, 없는 MAM_ENV_FILE로 (a) 실제 wrapper submit --agent human --dry-run의 registry timeout_sec, (b) 실제 LoopRunner(...).parse_args() 후 job_budget/contract_args()를 비교한다. 재현 코드: /tmp/3001f1d3-probe2.py.

기대: 동일 입력은 두 경로에서 같은 예산 또는 같은 기본값으로 정규화된다.

실제: 직접 wrapper는 30초로 등록했다. runner는 invalid 경고 후 3600초, ['--timeout', '3600']을 선택했다. U+00A0 및 U+3000도 helper 비교에서 같은 불일치를 보였다. LC_ALL=C에서는 Bash도 거부했다. 정상 ASCII 공백과 0700은 C/UTF-8 모두 정상이다. 현재 표 기반 테스트는 유니코드 공백과 로케일을 포함하지 않는다.

수정 방향: 문자 집합을 명시하고 로케일에 독립적인 한 규칙으로 통일한다. 브리프의 Python 판정 원칙을 따르면 공통 Python 정규화기를 Bash가 호출하는 방식이 적합하다. Bash 구현을 유지해야 한다면 최소한 함수 내부 C 로케일/명시적 ASCII 공백 등으로 범위를 고정하고 두 로케일에서 실제 wrapper와 runner를 비교한다. C 로케일에서 위 불일치가 없어지는 것은 별도 탐침으로 확인했다.

R3 — [P2] 명시적으로 전달한 빈 정수 플래그가 검증을 우회한다

근거: .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:197의 [ -n "${!_var}" ] || continue는 미지정 optional 값과 사용자가 전달한 빈 값을 구분하지 않는다. :201은 빈 idle timeout을 기본값으로 다시 만든다.

재현: 격리된 임시 환경에서 실제 래퍼를 submit --agent human --dry-run --prompt probe --registry-dir <tmp> --idle-timeout ''로 실행한다. 다른 네 플래그도 MAM_DELEGATE_JOB_NO_MAIN=1로 실제 래퍼를 source한 뒤 parse_opts <flag> ''로 확인했다.

기대: 명시적 빈 문자열은 양의 10진수 플래그가 아니므로 등록 전에 rc 1과 해당 플래그 오류 메시지.

실제: --idle-timeout ''은 rc 0으로 잡을 등록하고 idle_timeout_sec=120을 기록했다. 다섯 플래그 모두 parse_opts에서 수락된다. --heartbeat '' 또는 --heartbeat-misses ''는 그 단계에서 파생 idle timeout을 0으로 만든다. 후속 argparse가 일부 빈 값을 거부하더라도 새 검증의 일관된 rc 1 계약을 충족하지 못한다.

수정 방향: 명시적 플래그 값은 소비 시 검증하거나 presence를 기록하여 빈 값도 검증한다. 미지정 --idle-timeout에만 파생 기본값을 적용한다. 다섯 플래그의 빈 문자열을 현재 invalid-value 표에 추가하고 잡 미등록까지 검증한다.

R4 — [P2] NUL이 들어간 값은 Python 로더를 종료시키고 Bash에서는 다른 값이 된다

근거: .agents/skills/lib_py/delegation/envfile.py:69–:72는 NUL을 값으로 수락한다. :82의 실제 os.environ 대입은 ValueError를 발생시키지만 :94의 일반 dict는 수락하며 :98에서 그대로 출력한다. mqtt_common.py:100에는 이 예외 처리가 없다.

재현: 임시 파일을 정확히 b'GOOD=ok\nVALUE=left\x00right\nAFTER=yes\n' 바이트로 쓰고 같은 파일을 두 진입점에 지정한다. 실제 wrapper를 source한 후 세 환경값을 출력하고, 별도 Python 프로세스에서 mqtt_common.ensure_env_loaded()를 호출한다. 재현 코드: /tmp/3001f1d3-probe.py.

기대: OS 환경변수로 표현할 수 없는 값은 공통 파서에서 값 없는 줄 번호 경고와 함께 거부되거나, 두 진입점 모두 같은 명시적 실패를 반환한다.

실제: wrapper는 rc 0, VALUE=leftright, AFTER=yes. Python은 rc 1, ValueError: embedded null byte로 종료하여 뒤의 설정을 적용하지 못한다. 현재 “하나의 입력 → 같은 값/경고” 계약 밖의 차이다.

수정 방향: 공통 파서에서 NUL을 거부한다. 값이나 전체 원문을 경고에 포함하지 않는다. 정상 앞뒤 줄 처리와 두 실제 진입점의 동일 동작을 회귀 검증한다.

브리프의 7개 검토 항목

  1. 파서 규칙: envfile.py:50–:83이 양쪽의 공통 구현이다. 키/값 바깥 공백 제거, 한 쌍의 따옴표 제거, 값 내부 =, 빈 값, export KEY= 거부, 잘못된 키/줄 경고, 중복 키의 첫 값 우선은 코드와 기존 테스트에서 일치한다. CRLF는 두 줄을 정상 파싱했고 BOM은 첫 키를 invalid key로 건너뛰었다(탐침 확인). BOM 지원을 약속한 문서는 없으므로 이것 자체를 결함으로 판정하지 않았다. R4의 표현 불가능한 값은 불일치한다. .mam.env.example:32, :36, :49, :110의 경로 예시는 확장 없는 절대경로로 바뀌어 파서와 맞는다. 다만 템플릿 헤더 :10에는 env 우선, shell expansion 없음, export/inline comment/중복 키/BOM 규칙 설명이 없다. 사용자 문서에 문법과 이전 source 방식에서의 이행 예를 추가하는 것을 권고한다.

  2. eval 경로: envfile.py:21, :63, :66, :98의 키 fullmatch/예약 변수 차단/shlex.quote가 일반 문자열의 명령 치환과 따옴표 탈출을 막는다. wrapper :42는 자신의 세 경로 변수를 parser env에 넣어 파일로 교체되지 않게 한다. 일반 값과 예약 변수 공격 테스트에 우회는 관찰하지 않았다. -I는 stdlib-only 파일을 절대 경로로 실행하므로 이 파서에는 PYTHONPATH 불필요하며 격리가 적절하다. Python 실행 불가 시 wrapper rc 1+ERROR는 테스트로 검증된다. 파일 읽기 실패는 R1, NUL은 R4와 같이 별도 수정이 필요하다.

  3. 우선순위 변경: envfile.py:81의 key 존재 검사로 기존 env가 빈 문자열이어도 파일을 이긴다. 이전 wrapper source는 파일이 덮어썼으므로 의도적인 호환성 변경이다. runner의 env 계약(runner.py:477) 및 creator에 전달되는 control-plane 네 env(lib_py/control_plane.py:24, :70; create script :230, :242)를 작업 트리 파일이 덮어쓰지 않게 된다는 이점이 있다. 반대로 기존 pane의 broker/budget env가 오래됐으면 파일 수정만으로 바뀌지 않으므로 unset 또는 새 환경이 필요하다. Python remote 진입점(remote.py:463)은 이전에도 env 우선이었다. 테스트 env/MAM_ENV_FILE 격리와도 맞는다. 호환성 변경은 코드 주석과 테스트(test_issue26_envfile.py:184)에는 있지만 사용자용 템플릿/README에는 설명이 추가되지 않았다.

  4. 정수 노브: tests/test_issue26_ints.py:35의 표에 해당하는 shell arithmetic 입력은 공통 Bash helper를 통과한다. 예산/heartbeat/misses/ack 및 명시적 timeout 플래그의 min=1, inject/onboard/SKS 대기의 min=0은 기존 의미와 맞는다. 유효 9자리 경계와 선행 0 제거는 큰 입력을 int/arithmetic 전에 제한한다. 다만 R2/R3 때문에 “같은 규칙”은 아직 성립하지 않는다. PR 범위 밖의 기존 Python 노브(runner.py:580 retry, :628 wait grace, :749 child wait, :752 tree max)와 loop-count CLI(:208, :223, :235)는 이 helper를 사용하지 않는다. 따라서 모든 정수 설정이 통일됐다고 문서화하면 안 된다. 이들은 기존 코드이며 이번 변경의 신규 회귀로 별도 반려하지 않는다.

  5. 로컬 MQTT: tests/_local_mqtt.py:90의 localhost port 0 bind가 포트 배정을 OS에 맡긴다. :180 이후 실제 CONNECT/CONNACK, QoS 0/1, +/#, SUBACK, retained 저장/재전달/빈 payload 삭제, UNSUBACK, PING을 구현한다. PUBACK 전에 route하는 순서와 실제 paho 응답 대기 테스트(test_local_mqtt.py:100, :140, :173)가 wire 동작을 검증한다. 변경된 기존 테스트에서 의미 있는 단언 삭제는 찾지 못했다. 외부 브로커 실패 skip을 실패로 바꾸고 remote/heartbeat 수신 감사 로그 단언을 추가했다(test_issue18_liveness_contract.py:1098, :1228; test_issue19_subscriber_daemon_lifecycle.py:422). static scan은 tests/*.py 범위의 재유입 방지이지 모든 가능한 외부 네트워크를 차단하는 sandbox는 아니다. session fixture는 env를 복원하고 broker를 stop한다(conftest.py:1569); 이 호스트의 별도 start/stop 탐침에서 0.4초 후 broker thread 잔류는 없었다. 구현은 daemon thread를 join하지 않으므로 모든 플랫폼/종료 경합에 대한 누수 부재까지 증명한 것은 아니다. TLS/auth/persistent session/QoS2 등은 명시적 비지원 범위다.

  6. 기준 실패 주장: 이 호스트의 head와 base에서 tests/test_issue10_13_purge.py는 각각 23 passed, 실패 0건이었다. base 파일과 테스트 fixture를 archive 사본에서 실행했다. UUID4 및 임시 HOME/registry 격리를 먼저 확인했다(test_issue10_13_purge.py:51, :138; conftest.py:193). 따라서 “13개 실패 중 12개가 이 호스트의 기준 purge 실패”는 이번 검증으로 뒷받침되지 않는다. 과거 실행의 환경 차이/로그 없이는 그 당시 결과를 부정하거나 나머지 1건 원인을 단정할 수도 없다. PR 본문에 현재 재현 결과 또는 당시 명령·환경 차이를 명시해야 한다.

  7. 설계 경계 #17/#42: 파일 문법 판정의 Python 단일화는 목표와 맞는다. 정수 부분은 lib.sh:2345/:2359에 새로운 수락/거부/기본값/경고 판정을 두고 runner.py:71에 별도 구현을 복제했다. 따라서 브리프의 “판정은 Python” 경계에 완전히 부합하지 않으며 R2가 실제 drift 증거다. 공통 Python 규칙으로 정리하거나 제한적 Bash 예외를 명시적으로 합의해야 한다. 이 평가는 브리프가 제시한 합의와 저장소 설계 문서(docs/design/worktree-creator-model.md:116)를 기준으로 하며, 원격 이슈 댓글 원문까지 독립 확인했다고 주장하지 않는다.

검증 결과와 재현 산출물

  • 브리프 지정 7개 모듈: 228 passed, 1 skipped in 527.11s, rc 0. 유일한 skip은 macOS에서 /proc 기반 process-tree 테스트를 제외하는 tests/test_issue30_stall_watch.py:40 조건이다.
  • 추가 변경 모듈(test_issue19_subscriber_daemon_lifecycle.py, test_issue6_fail_closed.py, test_b19_headless_reconcile_fixes.py): 61 passed in 173.09s, rc 0.
  • purge 비교: head 23 passed in 11.24s, base 23 passed in 9.96s, 모두 rc 0.
  • git diff --check <base> HEAD, 두 변경 shell 파일의 bash -n, 변경 Python 파일의 AST syntax 검사: 모두 통과.
  • 테스트 종료 후 검증 트리 git status --short와 git diff --quiet: clean.

지정 테스트 명령(위 검증 cwd):

AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python \
PYTHONDONTWRITEBYTECODE=1 \
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python \
-m pytest -p no:cacheprovider -q --basetemp=/tmp/3001f1d3-selected \
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

로그: /tmp/3001f1d3-selected.log, /tmp/3001f1d3-migrated.log. 추가 모듈도 같은 환경/옵션에 별도 --basetemp=/tmp/3001f1d3-migrated를 사용했다. 실제 .mam.env는 export하지 않았다.

추가 탐침 로그:

  • /tmp/3001f1d3-probe.log: 빈 플래그, NUL, CRLF/BOM, invalid UTF-8, Unicode whitespace, broker stop.
  • /tmp/3001f1d3-probe2.log: 실제 dry-run wrapper와 runner의 예산 차이, 실제 빈 idle-timeout 등록, PermissionError.
  • /tmp/3001f1d3-purge-head.log: 23 passed in 11.24s.
  • /tmp/3001f1d3-purge-base.log: 23 passed in 9.96s.

탐침 재실행은 위 검증 cwd에서 PYTHONDONTWRITEBYTECODE=1 <위 인터프리터> /tmp/3001f1d3-probe.py "$PWD" 및 probe2.py로 가능하다. 모두 자체 임시 디렉터리와 합성 설정만 사용한다. 전체 약 75분 스위트는 지시대로 실행하지 않았다.

[VERDICT: NOT PASS]