# Follow-ups #41/#43 review — b5db3aee **NOT PASS**: F1/F3/F4/F5/F6는 요구사항을 충족한다. F2의 첫 호출은 요구한 오류 형식을 반환하지만, 실패를 성공으로 캐시하여 같은 프로세스의 다음 호출이 설정 검사를 우회한다. 아래 R1의 작은 수정과 회귀 테스트가 필요하며 설계 재계획은 필요하지 않다. ## 검증 identity - loop: `loop-20261001T230324Z-b511bf` - base: `0051fd6c3b088f8b265928ccd94ea31053fdde7e` - head: `95eabc693f7346f491592b053487bdcf0acd5f4f` - tree: `46399844fa63d62d97f7b52ee35010f2fa1b7998` - 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T230324Z-b511bf/1` - 인터프리터: `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/followups/.venv/bin/python` 브리프 identity와 일치함을 확인했다. 실제 설정·세션·모니터를 조작하지 않았고 `.mam.env`를 읽거나 export하지 않았다. 검증 트리와 creator/main 소스는 수정하지 않았다. 회귀 역검증과 탐침은 별도 `/tmp` archive 사본 또는 독립 Python 프로세스의 메모리에서 수행했다. ## R1 — [P2] F2: 설정 읽기 실패를 성공으로 캐시하여 재호출이 검사를 우회한다 **근거:** `.agents/skills/lib_py/delegation/mqtt_common.py:125`는 `_load_dotenv()`가 성공하기 **전**에 `_env_loaded=True`로 설정한다. 이번에 추가한 `load_env_or_report()`(`:135`–`:140`)는 `EnvFileError`를 잡아 `False`를 반환하면서 이 상태를 복구하지 않는다. 따라서 `registry.main()`(`.agents/skills/lib_py/delegation/registry.py:535`–`:540`) 등을 같은 프로세스에서 다시 호출하면 helper가 파일을 읽지 않고 `True`를 반환한다. **재현:** 검증 cwd에서 다음 합성 파일을 사용하는 `/tmp/b5db3aee-probe-cache.py`를 실행했다. 1. 임시 파일에 `b'EXPECTED_CONFIG=from-file\nBAD=\xff\n'`을 쓰고 `MAM_ENV_FILE`로 지정한다. WORKSPACE_ROOT/jobs/logs도 임시 경로에 둔다. 2. 하나의 Python 프로세스에서 실제 `registry.main(['--registry-dir', , 'register', '--prompt', 'synthetic retry probe'])`를 두 번 호출한다. 예외를 임의로 삼키거나 제품 함수를 대역으로 바꾸지 않는다. 3. 파일을 유효한 `EXPECTED_CONFIG=from-file`로 복구한 뒤 `load_env_or_report()`를 호출한다. | 단계 | 기대 | head 실제 결과 | |---|---|---| | 깨진 파일, 첫 register | rc 1, 오류 한 줄, 등록 없음 | rc 1, 등록 0건. 그러나 `_env_loaded=True` | | 같은 깨진 파일, 두 번째 register | 다시 rc 1, 등록 없음 | **rc 0, 잡 1건 등록**. 설정 파일 오류가 출력되지 않음 | | 파일 복구 후 재시도 | 파일을 읽어 EXPECTED_CONFIG 적용 | helper가 True를 반환하지만 EXPECTED_CONFIG는 여전히 미설정 | 한 번 실행하고 바로 끝나는 CLI 프로세스에서는 드러나지 않는다. 하지만 새 helper는 호출자를 종료시키지 않고 실패값을 반환하며, 실제 `main()` 함수도 일반 함수로 호출된다. 실패 후 재시도·복구가 가능한 동일 프로세스에서 “존재하지만 읽지 못한 파일이면 rc 1” 계약을 깨고 설정 없는 작업을 수행한다. `_env_loaded`의 선행 대입 자체는 기존 코드이나, 이번 F2가 예외를 정상 반환값으로 전환하면서 함께 처리해야 하는 상태 불변식이다. **수정 방향:** `_env_loaded=True`를 `_load_dotenv(workspace_dir)`의 **성공 뒤**로 옮겨 성공만 캐시한다. 공통 helper에서 실패 시 캐시를 되돌리는 방법도 가능하지만, 성공 뒤에 기록하는 쪽이 라이브러리 직접 호출까지 일관되게 해결한다. 동일 프로세스에서 (a) 연속 실패 두 번 모두 rc 1, (b) 파일 복구 후 설정 적용, (c) 성공 후에는 기존 once-per-process 동작 유지, (d) 실패 중 registry 생성 없음까지 검증한다. **대안 검증:** `/tmp/b5db3aee-probe-cache-fixed-in-memory.py`에서 해당 두 문장의 순서만 메모리 내 함수에 바꾸고 같은 탐침을 실행했다. 결과는 rc **1 → 1**, 등록 **0 → 0**, 실패 중 cache=False, 복구 후 `EXPECTED_CONFIG=from-file`이었다. 검증 트리 파일은 변경하지 않았다. 원본 및 대안 로그는 각각 `/tmp/b5db3aee-probe-cache.log`, `/tmp/b5db3aee-probe-cache-fixed.log`이다. ## F1~F6 완료 조건 검토 | 항목 | 판단 | 근거 및 회귀 검증 | |---|---|---| | F1 날짜 의존 테스트 | 충족 | `tests/test_issue21_acceptance.py:268`의 `_marker_lstart`가 공백을 정규화한다. `:259` ps 대역은 날짜에 관계없이 `Oct 1`을 만들며 `:275`는 real/padded 두 경로를 검증한다. 실제 marker 충돌 거부, 미등록, marker 보존 단언이 유지됐다. `loop_lock.sh`의 `mam_lstart`/holder 비교는 변경하지 않았다(F4의 설정 초기화만 변경). 비슷한 패턴 검색 결과 `test_o3_scoped_guard.py:57`은 이미 정규화하고, `test_o2_race_free_lock.py:40`의 raw helper는 호출되지 않는 정의다. 불필요한 정리는 하지 않아도 된다. | | F2 Python 오류 한 줄/rc 1 | **미완료: R1** | 일곱 main 모두 공통 helper를 호출하며 라이브러리가 sys.exit하지 않는다. 새 프로세스에서 권한/UTF-8 오류 14개 조합은 한 줄/rc 1/값 비노출을 검증한다(`tests/test_issue26_envfile.py:184`). job_subscriber의 `RC_ERROR`는 1로 계약과 맞는다. 다만 실패 캐시와 동일 프로세스 재호출을 다루는 테스트가 없다. | | F3 BOM/중복/문서 | 충족, 아래 경미한 문구 권고 | `envfile.py:61`의 `utf-8-sig`가 첫 BOM만 제거한다. key-first-wins와 명시 파일 부재의 no-fallback 동작은 유지된다. `.mam.env.example:24`의 BOM, `:26`의 첫 값 우선 및 `MESSAGING.md:369`의 부재/읽기 실패 구분이 반영됐다. `tests/test_issue26_envfile.py:414` 이후 세 테스트가 실제 두 로더와 문서를 함께 확인한다. | | F4 정수 범위 확장 | 충족 | `runner.py:627`, `:630`, `:762`, `:766`에서 grace/child wait min=0, tree max min=1; invalid는 한 번 경고 후 기본값 300/예산+grace/50. 0은 각각 여유 없음/한 번만 확인으로 의미가 분명하다. Bash helper를 함수만 있는 `lib_int.sh`로 옮겨 standalone lock/diff scripts에도 제공한다. ready/empty streak min=1, idle retries/backoff 및 lock retries min=0, diff caps/poll interval min=1은 소비처 의미와 맞는다. 기본 0.5초인 `SKS_QUIESCENT_INTERVAL`은 원래 소수 의미를 유지하고 표에 이유를 남겼다. 저장소 내 backoff 사용은 0초, poll 사용은 정수이며 이들의 소수 사용 사례는 발견하지 못했다. loop SKILL에 grace와 child 설정이 문서화됐다. | | F5 제품 테스트 훅 제거 | 충족 | `stall_watch.py:225`–`:228`은 실제 adapter 속성만 읽는다. `tests/test_issue30_stall_watch.py:196`의 별도 테스트 프로세스에서 adapter property를 False로 바꾼 뒤 실제 watcher main을 실행한다. S13의 “idle에서는 무발행/무주입, blocked는 발행” 단언을 유지하고 `:475`에서 제품의 MAM_TEST_ 부재를 고정한다. | | F6 문서/두 번째 wait 중 소멸 | 충족 | delegate-job `SKILL.md:53`은 60초를 herdr wait에 한정하고 발행 완료까지 더 늦을 수 있음을 명시한다. `test_issue30_stall_watch_stop.py:429`가 두 번째 wait 내부에서 gone 상태를 바꾸고 `:435`가 실제 두 번의 wait 및 마지막 alive=False를 확인한다. 단순 호출 횟수만으로 wait 전에 종료되는 이전 허점을 막는다. | 위 표의 짧은 제품 경로는 `.agents/skills/` 아래이며 runner는 `lib_py/loop/runner.py`, envfile은 `lib_py/delegation/envfile.py`이다. **경미한 문서 권고:** 새 `MESSAGING.md:369`은 wrapper와 Python이 모두 오류 **한 줄**을 출력한다고 서술한다. Python의 새 형식은 한 줄이지만 wrapper의 기존 `:44`는 parser ERROR 뒤에 `ERROR: cannot load ... (see the line above)`를 한 줄 더 출력한다. 한 줄이라는 표현을 Python 진입점에만 한정하면 정확하다. 이번 반려의 근거는 R1이며 이 문구는 별도 blocker가 아니다. ## 수정 전 실패 확인 방법 - base `0051fd6`을 `/tmp/b5db3aee-before`에 `git archive`로 풀고, 현재 회귀 테스트 3개 파일(envfile/ints/stall_watch)만 복사했다. F2의 7×2 오류 조합, F3의 세 항목, F4의 runner/standalone/empty capture, F5의 제품 훅 부재를 선택했다. **44 failed, 17 passed, 118 deselected in 72.16s**. 테스트 수집/누락 모듈 실패가 아니라 traceback, BOM 유실, 문서 불일치, 경고/기본값/정규화 차이 및 제품 훅 잔존을 확인하는 단언에서 실패했다. 정상 값 일부는 원래도 같으므로 통과하는 것이 맞다. 긴 child wait 값은 이전 구현에서 60초 subprocess timeout으로 실패했다. 로그: `/tmp/b5db3aee-before.log`. - F3 중 중복 키와 없는 명시 파일은 동작을 바꾸지 않는 문서 수정이다. 이전 소스에서는 동작 단언은 통과하고 새 문서 단언이 실패했다. 이를 기능 회귀로 과장하지 않는다. - F1은 제품 수정이 아닌 테스트 수정이다. `/tmp/b5db3aee-test-mutations`에서 새 테스트의 marker 정규화만 이전 `.strip()`으로 되돌리면 padded 테스트가 실패한다. 정규화 결과 자체를 확인하는 단언을 임시로 제거하고 한 번 더 실행해도 실제 `already running` 거부 단언에서 실패했다(잘못된 stale 판정으로 controller 초기화까지 진행). 날짜에 무관하게 이전 문제를 재현했다. 로그: `/tmp/b5db3aee-f1-behavior.log`. - F6 역시 제품 동작 변경이 아닌 테스트의 사건 순서 강화다. 같은 임시 사본에서 alive 대역을 이전 `호출 횟수 <= 2` 방식으로 되돌리면 새 `ticks == [1, 2]` 단언이 `[1]`로 실패했다. 새 테스트가 이전의 잘못된 사건 배치를 구별함을 확인했다. F1/F6 mutation 첫 실행은 **2 failed**였다. 로그: `/tmp/b5db3aee-test-mutations.log`. ## 범위와 검증 결과 8개 커밋은 모두 지정된 `fix(followups):`/`test(followups):` 접두어와 Co-Authored-By를 갖는다. 항목별 커밋이며 마지막 F2 보완은 subscriber의 symbolic rc 정리다. 공통 `lib_int.sh` 추출에 따라 standalone 테스트 fixture에 파일을 복사하고 `deploy/remove.sh` fallback에 새 파일을 추가한 것은 필요한 부수 변경이다. 설치와 freeze는 skills 디렉터리를 통째로 복사하므로 새 파일도 포함된다. wrapper 종결 처리 Python 이전, #39/#40/#42, 버전 bump나 외부 상태 변경은 없다. - 브리프 지정 7개 파일 + 직접 변경된 `test_issue21_loop_b.py`: **303 passed, 1 skipped in 446.40s**, rc 0. skip은 macOS에서 `/proc` 기반 process-tree 테스트를 제외하는 기존 조건이다. 로그: `/tmp/b5db3aee-selected.log`. - 공통 Bash helper/lock/poll 변경의 추가 검증(`test_o2_race_free_lock.py`, `test_issue6_turn_handshake.py`, `test_issue19_subscriber_daemon_lifecycle.py`): **52 passed in 102.02s**, rc 0. 로그: `/tmp/b5db3aee-extra.log`. - 검증 종료 후 `git status --short`/`git diff --quiet`는 clean이며 HEAD/tree도 동일하다. 지정 검증 명령(위 review cwd): ```bash AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/followups/.venv/bin/python \ PYTHONDONTWRITEBYTECODE=1 \ /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/followups/.venv/bin/python \ -m pytest -p no:cacheprovider -q --basetemp=/tmp/b5db3aee-selected \ tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_issue21_acceptance.py \ tests/test_issue30_stall_watch.py tests/test_issue30_stall_watch_stop.py \ tests/test_tier1_unit.py tests/test_version_consistency.py tests/test_issue21_loop_b.py ``` 추가 검증은 같은 환경/옵션에 별도 `--basetemp=/tmp/b5db3aee-extra`를 사용했다. 실제 `.mam.env` export 없이 local MQTT fixture를 사용했다. - `git diff --check 0051fd6 HEAD`: 통과. - 변경된 모든 Python 파일 AST 구문 검사와 shell 파일 `bash -n`: 통과. - 독립 cache probe: 원본에서 R1 재현, 메모리 내 최소 수정에서 해결 확인. - 전체 약 60분 스위트는 지시대로 실행하지 않았다. [VERDICT: NOT PASS]