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
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-02 00:23:22 +09:00
1 parent 0eec795cd1
commit 2e90c809d3
28 files changed
+922 -26

No files matched your search

@@ -0,0 +1,112 @@
# 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):
```bash
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]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261001T140030Z-34381f",
"base_sha": "e1eb22bd59ce825e3a22e86433c30f7ffd833810",
"head_sha": "0eec795cd1b46f7a28b9a1cc87ef8577619a731f",
"tree_sha": "584eac551f880232896b898cc43156f28ae1bacb",
"review_commit": "0eec795cd1b46f7a28b9a1cc87ef8577619a731f",
"reviewer": "planner-reviewer-codex-01",
"job_id": "95a22045",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-01T14:27:22Z"
}
@@ -0,0 +1,70 @@
# PR #43 2회차 리뷰 — job 95a22045
판정: **PASS**. 새 커밋 `0eec795`에서 이전 R1~R4가 모두 수정됐다. 해당 수정 범위에서 새 반려 사유는 발견하지 못했다.
## 검증 대상과 범위
- 검토 diff: `b58edace115205500c1197d9ce59cb5c6d724893..0eec795cd1b46f7a28b9a1cc87ef8577619a731f` (브리프의 codex 지시대로 수정 커밋만 검토).
- loop: `loop-20261001T140030Z-34381f`
- PR base: `e1eb22bd59ce825e3a22e86433c30f7ffd833810`
- head: `0eec795cd1b46f7a28b9a1cc87ef8577619a731f`
- tree: `584eac551f880232896b898cc43156f28ae1bacb`
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T140030Z-34381f/1`
- Python: `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python`
HEAD/tree가 브리프와 일치함을 확인했다. 코드와 브랜치는 변경하지 않았다. 실제 `.mam.env`를 읽거나 export하지 않았으며, 테스트·탐침은 합성 설정과 격리된 임시 HOME/registry/log 디렉터리를 사용했다. MQTT 구현과 purge 기준 비교는 이번 수정 커밋에 변경이 없어 전체 재리뷰하지 않았다. 다만 지정된 MQTT/liveness 회귀 테스트는 실행했다.
## 이전 지적의 종결 여부
| 지적 | 판정 | 수정 근거와 확인 결과 |
|---|---|---|
| R1: 읽을 수 없는 설정 파일에서 wrapper가 rc 0으로 계속 진행 | **종결** | `lib_py/delegation/envfile.py:60`은 전체 읽기·디코딩 실패를 `EnvFileError`로 올리고, `:107`은 CLI 오류 메시지와 rc 1로 바꾼다. wrapper `:44`도 rc 1로 종료한다. 이전 chmod-000 탐침이 이제 오류와 rc 1을 반환하며 usage를 출력하지 않았다. Python 로더 및 실제 `publish_event.py`에서도 권한/UTF-8 오류를 각각 검증했다. |
| R2: 로케일에 따른 Bash/Python 정수 해석 불일치 | **종결** | `lib.sh:2349`–`:2351`은 ASCII 공백 6개와 숫자 10개를 명시한다. 이전 U+2003 예산 탐침은 wrapper와 runner 모두 경고 후 3600으로 일치했다. 별도 24개 비교(ASCII 공백 6종, Unicode 공백/숫자, 선행 0; C와 en_US.UTF-8)도 모두 일치했다. `test_issue26_ints.py:190`의 회귀 검증은 helper뿐 아니라 실제 wrapper 등록과 runner budget을 비교한다. |
| R3: 명시적 빈 정수 플래그의 검증 우회 | **종결** | wrapper `:167`, `:175`–`:179`가 명시된 플래그를 기록하고 `:201`이 빈 값도 검증으로 보낸다. 이전 실제 `submit --dry-run --idle-timeout ''` 탐침은 이제 rc 1, 해당 플래그 오류, 잡 미등록이다. 테스트의 다섯 플래그 invalid 표에도 빈 문자열이 추가됐다(`test_issue26_ints.py:229`). 미지정 idle timeout의 파생 기본값은 유지된다. |
| R4: NUL 값이 Python을 종료시키고 Bash에서는 다른 값이 됨 | **종결** | `envfile.py:66`에서 환경 대입·shell 출력 전에 NUL 줄을 공통으로 건너뛴다. 이전 `GOOD=ok / VALUE=left<NUL>right / AFTER=yes` 탐침에서 두 실제 로더 모두 rc 0, VALUE 미설정, 앞뒤 값 보존, 줄 번호 경고를 확인했다. 값 문자열은 경고에 노출되지 않았다. |
위 표의 `lib.sh`, `lib_py/...`는 `.agents/skills/` 아래이며 wrapper는 `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job`이다. 테스트 경로는 `tests/` 아래다.
사용자 문서 보완도 확인했다. `.mam.env.example:14`–`:26`에 KEY=VALUE 규칙, 확장 없음, 절대경로 사용, inline `#`의 의미, export/예약 변수/NUL 거부, env 우선 및 전체 읽기 실패의 오류 처리가 명시됐다. 기존의 파서 규칙·우선순위 이행 설명 부족에 대한 권고는 해소됐다. env 우선 정책과 eval의 key 검증/shlex 인용은 이번 커밋에서 바뀌지 않았다.
## Python 진입점의 읽기 실패 처리에 대한 의견
**예외로 멈추는 현재 방향에 동의한다.** 존재하는 설정 파일을 읽거나 디코딩하지 못한 경우는 설정이 없는 경우와 다르다. 경고 후 계속하면 broker/auth/dispatch 설정이 누락된 상태에서 다른 기본 연결을 사용하거나 잘못된 제어 평면으로 이벤트를 보낼 수 있다. `EnvFileError`는 파일 경로와 오류 종류만 포함하고 `from None`으로 원본 디코딩 예외를 숨기며, 파일 전체를 파싱하기 전에 환경 대입이 일어나지 않는다(`envfile.py:57`, `:89`). 개별 잘못된 줄을 건너뛰는 규칙과 전체 파일 실패를 구분한 것이 적절하다.
실제 `publish_event.main`은 `publish_event.py:337`에서 설정을 로드한 뒤 잡 조회·보고서 finalize·발행으로 진행한다. 이번 탐침은 명시적 합성 broker 환경변수가 이미 있어도 깨진 파일의 오류가 가려지지 않고, CLI가 rc 1을 반환하며 임시 registry를 생성하지 않는 것을 확인했다. 권한 오류와 잘못된 UTF-8 모두 동일했다. 설정 값은 stderr에 노출되지 않았다.
운영상 새 publisher 또는 heartbeat 프로세스는 잘못된 파일 때문에 시작하지 못할 수 있고, 그 결과 열린 잡이 liveness timeout에 도달할 수 있다. 이는 설정 실패를 성공적인 이벤트 전달로 오인하지 않도록 하는 의도된 실패다. 파일 권한/인코딩을 복구하고 실패한 명령을 다시 실행해야 하며, 자동으로 기본 broker로 우회하는 동작은 권하지 않는다. 이미 설정을 로드한 장기 실행 프로세스는 once-per-process 방식(`mqtt_common.py:109`)에 따라 파일을 매 이벤트마다 다시 읽지 않는다.
현재 Python CLI의 traceback은 설정 실패를 드러내고 nonzero를 반환하므로 기능상 수용 가능하다. 향후 CLI 경계에서 `EnvFileError`를 잡아 짧은 오류 메시지와 rc 1로 바꾸면 사용자 경험은 좋아지겠지만, 경고만 남기고 실행을 계속하는 방식으로 바꾸면 안 된다. 이 표현 개선은 이번 PASS의 선행 조건이 아니다.
## 설계 경계와 범위 제한
이 수정 커밋은 새 업무 판단이나 제어 평면 분기를 Bash에 추가하지 않는다. 기존 정수 helper의 문자 집합을 고정하고 명시적 CLI 입력 여부를 기록한다. 정수 정규화가 Bash/Python 두 구현으로 남는 구조적 중복은 여전하므로 #17/#42의 Python 단일화 후속 작업 대상이다. 다만 1회차 R2에서 제시한 최소 수정 방향(명시적 ASCII와 실제 두 경로의 비교 검증)을 충족하므로 이를 새로운 반려 조건으로 확대하지 않는다. 파일 문법·NUL·전체 읽기 실패 판정은 공통 Python 모듈에 있다.
로컬 MQTT 브로커와 기존 22개 전환 테스트의 구현은 이번 커밋에서 변경되지 않았다. purge 기준 실패 주장 역시 이번 검토 범위 밖이며, 1회차의 base/head 각 23건 통과 결과를 새 실행 결과로 재표기하지 않는다. 전체 스위트는 지시대로 오케스트레이터에 맡겼다.
## 검증 결과
지정 테스트: **174 passed in 210.31s (3분 30초), rc 0, skip 없음**. 로그는 `/tmp/95a22045-selected.log`.
검증 cwd에서 실행한 명령:
```bash
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/95a22045-selected \
tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_local_mqtt.py \
tests/test_issue18_liveness_contract.py tests/test_tier1_unit.py
```
검증 종료 후에도 HEAD/tree는 위 identity 그대로이며 `git status --short`와 `git diff --quiet`는 clean이었다.
- `git diff --check b58edac 0eec795`: 통과.
- 변경 shell 파일의 `bash -n`, 변경 Python 파일 AST syntax 검사: 통과.
- 독립 탐침: `/tmp/95a22045-probes.py`, 출력 `/tmp/95a22045-probes.log`.
- 이전 실제 wrapper/runner 탐침 재실행 결과: `/tmp/95a22045-reprobe.log`.
독립 탐침은 검증 cwd에서 `PYTHONDONTWRITEBYTECODE=1 <위 Python> /tmp/95a22045-probes.py "$PWD"`로 재실행할 수 있다. 합성 파일, uuid4 잡 식별자, 자체 임시 디렉터리만 사용한다.
[VERDICT: PASS]