# πŸ” Review Report β€” PR #41Β·#43 후속 `fix/followups-41-43` (job `76ca3b1b`) - **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) Β· 2026-10-02 - **λŒ€μƒ**: `0051fd6..95eabc6` (8컀밋, 27파일 `+397/βˆ’88`, `git diff --check` 클린), base `main 0051fd6`. - **검증 경둜**: `/var/folders/.../T/mam-review/loop-20261001T230324Z-b511bf/1`. **identity 확인**: `HEAD = 95eabc69…d5f4f`, `HEAD^{tree} = 46399844…7998` β€” λΈŒλ¦¬ν”„μ™€ 일치. - **μ•ˆμ „ κ·œμΉ™ μ€€μˆ˜**: 검증 κ²½λ‘œΒ·μ†ŒμŠ€ μ›Œν¬νŠΈλ¦¬(`multi-agent-mux-wt/followups`)Β·λ³Έ 체크아웃 λͺ¨λ‘ μˆ˜μ •Β·μ»€λ°‹Β·checkout μ—†μŒ. 되돌린 사본·합성 파일·`ps` λŒ€μ—­μ€ scratchpad μ—μ„œλ§Œ. μ‹€μ œ `.mam.env` λ₯Ό μ½κ±°λ‚˜ 좜λ ₯ν•˜μ§€ μ•Šμ•˜κ³  export ν•˜μ§€ μ•Šμ•˜λ‹€. μ‹€μ œ `.mam`·데λͺ¬Β·μ„Έμ…˜Β·herdr μ‘°μž‘ μ—†μŒ, pushΒ·mergeΒ·PR μƒνƒœ λ³€κ²½ μ—†μŒ. ## κ²°λ‘  μš”μ•½ **F1–F6 λͺ¨λ‘ μ™„λ£Œ 쑰건을 λ§Œμ‘±ν•œλ‹€. λ²”μœ„ λ°– λ™μž‘ 변경은 μ—†λ‹€.** μ œν’ˆ μ½”λ“œλ₯Ό λ°”κΎΈλŠ” ν•­λͺ©(F2Β·F3Β·F4Β·F5)의 νšŒκ·€λŠ” μ œν’ˆ 파일만 base 둜 되돌린 μ‚¬λ³Έμ—μ„œ **47건 μ‹€νŒ¨**ν•œλ‹€(μ•„λž˜ Β§7). ν…ŒμŠ€νŠΈλ§Œ κ³ μΉ˜λŠ” ν•­λͺ©(F1Β·F6)은 base μ œν’ˆ μ½”λ“œμ—μ„œλ„ ν†΅κ³Όν•˜λŠ” 것이 정상이며, F1 은 μ˜› ν…ŒμŠ€νŠΈκ°€ λ‚ μ§œμ— 따라 μ‹€νŒ¨ν•˜λ˜ 원인을 `ps` λŒ€μ—­μœΌλ‘œ 직접 μž¬ν˜„ν–ˆλ‹€. 비차단 κ΄€μ°° 3건(T1–T3). ## F1 β€” A8 의 λ‚ μ§œ 의쑴 (`tests/test_issue21_acceptance.py:258-300`) - μ œν’ˆ μ½”λ“œ(`loop_lock.sh` 의 `mam_lstart`)λŠ” **κ·ΈλŒ€λ‘œ**λ‹€. `loop_lock.sh` 의 diff λŠ” F4b 의 `MAM_LOCK_STEAL_RETRIES` ν•œ 쀄뿐이닀. - ν…ŒμŠ€νŠΈλŠ” 마컀의 lstart λ₯Ό `" ".join(out.split())` 둜 μ¨μ„œ μ œν’ˆμ˜ `tr -s ' '` + μ•žλ’€ 곡백 μ œκ±°μ™€ κ°™κ²Œ λ§žμ·„λ‹€. - λ‚ μ§œμ™€ λ¬΄κ΄€ν•˜κ²Œ κ³ μ •ν•˜λŠ” μž₯치: `padded` νŒŒλΌλ―Έν„°κ°€ PATH μ•žμ— `ps` λŒ€μ—­μ„ λ‘”λ‹€. 이 λŒ€μ—­μ€ `lstart=` μ§ˆμ˜μ—λ§Œ `Thu Oct 1 08:02:03 2026`(두 μΉΈ)을 돌렀주고, λŒ€μ—­μ΄ μ‹€μ œλ‘œ μ“°μ˜€λŠ”μ§€λ„ λ‹¨μ–Έν•œλ‹€. - **μ˜› 버그 μž¬ν˜„(직접)**: 같은 λŒ€μ—­ μ•„λž˜μ—μ„œ μ œν’ˆ `mam_lstart` λŠ” `Thu Oct 1 …`(ν•œ μΉΈ), μ˜› ν…ŒμŠ€νŠΈ 방식(`.strip()`)은 `Thu Oct 1 …`(두 μΉΈ)을 λ‚Έλ‹€ β†’ 뢈일치 β†’ 잠금이 "stale" 둜 보여 두 번째 루프가 κ±°λΆ€λ˜μ§€ μ•ŠλŠ”λ‹€. 즉 μ˜› A8 이 1~9일에 μ‹€νŒ¨ν•œ 원인이 μ •ν™•νžˆ 이것이고, μƒˆ `padded` μΌ€μ΄μŠ€λŠ” 맀일 κ·Έ 쑰건을 λˆλ‹€. - 같은 νŒ¨ν„΄μ˜ λ‹€λ₯Έ ν…ŒμŠ€νŠΈ: `test_o3_scoped_guard.py:_get_lstart` λŠ” 이미 `" ".join(...split())` 둜 μ •κ·œν™”ν•œλ‹€. `test_o2_race_free_lock.py:get_lstart` λŠ” μ •κ·œν™”ν•˜μ§€ μ•Šμ§€λ§Œ **μ–΄λ””μ„œλ„ ν˜ΈμΆœλ˜μ§€ μ•ŠλŠ” 죽은 헬퍼**λ‹€(T1, μ§€μš°κ±°λ‚˜ μ •κ·œν™” ꢌ고). ## F2 β€” μ§„μž…μ μ˜ ν•œ 쀄 였λ₯˜ (`mqtt_common.py:129-140` μ™Έ 7κ³³) - 곡톡 헬퍼 `load_env_or_report()` κ°€ `EnvFileError` λ₯Ό μž‘μ•„ `ERROR: : cannot read ()` ν•œ 쀄을 좜λ ₯ν•˜κ³  False λ₯Ό λŒλ €μ€€λ‹€. 라이브러리 ν•¨μˆ˜λŠ” `sys.exit` ν•˜μ§€ μ•ŠλŠ”λ‹€ β€” μ§„μž…μ (`publish_event`Β·`registry`Β·`job_subscriber`Β·`cli`Β·`bench`Β·`heartbeat`Β·`remote` 의 `main`)이 `return 1` ν•œλ‹€. `job_subscriber` λŠ” `RC_ERROR`(=1)둜 이름을 썼닀. - κ°’Β·νŒŒμΌ λ‚΄μš©μ€ 좜λ ₯λ˜μ§€ μ•ŠλŠ”λ‹€(μ˜ˆμ™Έ λ©”μ‹œμ§€κ°€ κ²½λ‘œμ™€ μ˜ˆμ™Έ μ’…λ₯˜λΏ). - νšŒκ·€ `test_a1_…[7 μ§„μž…μ  Γ— permission/utf8]` 14건이 base μ—μ„œ λͺ¨λ‘ μ‹€νŒ¨ν•œλ‹€. - reconcile.sh 의 λ‚΄μž₯ κ΅¬λ…μžλŠ” μ—¬μ „νžˆ `ensure_env_loaded()` λ₯Ό 직접 λΆ€λ₯Έλ‹€(λΈŒλ¦¬ν”„ λͺ©λ‘ λ°–, λ¬Έμ„œν™”λœ κ·ΈλŒ€λ‘œ). ## F3 β€” `.mam.env` μ„ΈλΆ€ - (a) BOM: `envfile.py:60-61` 이 `utf-8-sig` 둜 μ—°λ‹€. 곡톡 νŒŒμ„œλΌ **두 λ‘œλ” λͺ¨λ‘** μ μš©λœλ‹€. 파일 맨 μ•žμ˜ BOM 만 λ²—κΈ°λ―€λ‘œ μ€‘κ°„μ˜ U+FEFF λŠ” κ·ΈλŒ€λ‘œλ‹€. - (b) 쀑볡 ν‚€λŠ” 첫 κ°’ μš°μ„ : λ™μž‘μ€ κ·ΈλŒ€λ‘œ(`apply` κ°€ `if key not in environ`)이고 `.mam.env.example` FORMAT μ ˆμ— ν•œ 쀄 μΆ”κ°€ν–ˆλ‹€. - (c) `MESSAGING.md:369`: "λͺ…μ‹œν•œ 파일이 μ—†μœΌλ©΄ 였λ₯˜" λ₯Ό "쑰용히 κ±΄λ„ˆλœ€(μ£Όλ³€ 탐색은 μ•ˆ 함), μžˆλŠ”λ° λͺ» 읽으면 EnvFileError" 둜 μ‹€μ œ λ™μž‘μ— λ§žμ·„λ‹€. - νšŒκ·€ `test_a7_*` 3건이 base μ—μ„œ μ‹€νŒ¨ν•œλ‹€. ## F4 β€” μ •μˆ˜ λ…ΈλΈŒ - (a) λŸ¬λ„ˆ(`runner.py:627`, `:630-643`, `:762-768`): `_int_knob` 이 `_positive_int` κ·œμΉ™μœΌλ‘œ 읽고, 잘λͺ»λœ 값이면 λ…ΈλΈŒλ‹Ή ν•œ 번 κ²½κ³ ν•˜κ³  기본값을 μ“΄λ‹€. `MAM_CHILD_WAIT_S=abc` 의 `ValueError` κ°€ μ‚¬λΌμ‘Œλ‹€. - μ΅œμ†Ÿκ°’μ˜ μ˜λ―Έλ„ λ§žλ‹€. | λ…ΈλΈŒ | μ΅œμ†Ÿκ°’ | 의미 | |---|---|---| | `MAM_WAIT_GRACE_S` | 0 | μ˜ˆμ‚°λ§ŒνΌ μ •ν™•νžˆ λŒ€κΈ° | | `MAM_CHILD_WAIT_S` | 0 | ν•˜μœ„ μž‘μ„ ν•œ 번만 확인 | | `MAM_JOB_TREE_MAX` | 1 | 0 이면 λͺ¨λ“  νŠΈλ¦¬κ°€ "초과"; λŒ€κΈ°ν•˜μ§€ μ•ŠμœΌλ €λ©΄ `MAM_CHILD_WAIT_S=0` | μ„Έ κ°’ λͺ¨λ‘ ν‘œ `RUNNER_INT_KNOBS` 에 λ“€μ–΄κ°”λ‹€. - (b) bash λ…ΈλΈŒ: `MAM_READY_EMPTY_GIVEUP`Β·`SKS_EMPTY_GIVEUP`(min 1), `SKS_IDLE_RETRIES`Β·`SKS_IDLE_RETRY_BACKOFF_S`(min 0), `MAM_LOCK_STEAL_RETRIES`(min 0), `MAM_DIFF_MAX_BYTES/LINES`(min 1), `RECONCILE_POLL_INTERVAL`(min 1 β€” 0 이면 λ°”μœ 루프)κ°€ `mam_int_knob` 을 κ±°μΉœλ‹€. - `SKS_QUIESCENT_INTERVAL` 은 기본값이 μ†Œμˆ˜(0.5)라 **κ·ΈλŒ€λ‘œ 두고 ν‘œμ— μ‚¬μœ **λ₯Ό 남겼닀 β€” λΈŒλ¦¬ν”„ μ§€μ‹œλŒ€λ‘œλ‹€. - `RECONCILE_POLL_INTERVAL` 은 μ €μž₯μ†Œ μ•ˆμ˜ λͺ¨λ“  μ‚¬μš©μ²˜(ν…ŒμŠ€νŠΈ `60`, λ¬Έμ„œ 15 s)κ°€ μ •μˆ˜λΌ μ˜λ―Έκ°€ κΉ¨μ§€μ§€ μ•ŠλŠ”λ‹€. - `diff_collect.sh`Β·`loop_lock.sh` λŠ” lib.sh 없이 source λ˜λ―€λ‘œ κ·œμΉ™μ„ `lib_int.sh` 둜 λΆ„λ¦¬ν–ˆλ‹€. lib.sh 도 같은 νŒŒμΌμ„ source ν•œλ‹€(`declare -F` κ°€λ“œλ‘œ 이쀑 μ •μ˜λ₯Ό λ§‰μŒ). `deploy/remove.sh` 의 폴백 μžμ‚° λͺ©λ‘μ— `lib_int.sh` λ₯Ό μΆ”κ°€ν•œ 것은 μƒˆ νŒŒμΌμ„ 배포 제거 λŒ€μƒμ— λ„£λŠ” **ν•„μˆ˜ λ™λ°˜ λ³€κ²½**이닀(λ²”μœ„ μ•ˆ). `test_issue21_loop_b.py` 의 ν•œ 쀄도 L15 κ°€ `diff_collect.sh` λ₯Ό 볡사해 μ“°λ―€λ‘œ ν•„μš”ν•œ λ™λ°˜ μˆ˜μ •μ΄λ‹€. - (c) loop SKILL.md 의 Job budget 행에 `MAM_WAIT_GRACE_S`(κΈ°λ³Έ 300, 0 이상, 잘λͺ»λœ 값은 κ²½κ³  ν›„ 300)λ₯Ό λ¬Έμ„œν™”ν–ˆλ‹€. - νšŒκ·€ `test_b1_runner_*`Β·`test_b5_*` κ°€ base μ—μ„œ μ‹€νŒ¨ν•œλ‹€. ## F5 β€” ν…ŒμŠ€νŠΈ μŠ€μœ„μΉ˜ 제거 (`stall_watch.py`, `test_issue30_stall_watch.py`) - `MAM_TEST_UNRELIABLE_AGENT` 두 쀄을 μ‚­μ œν–ˆλ‹€. - S13 은 `-c` μ§„μž…μ μ—μ„œ μ–΄λŒ‘ν„° 클래슀의 `agent_status_reliable` 을 `False` 둜 λ°”κΎΌ λ’€ **μ‹€μ œ** `stall_watch.main` 을 λˆλ‹€. μ œν’ˆμ—λŠ” λΆ„κΈ°κ°€ μ—†κ³ , κ²€μ¦ν•˜λŠ” λ‚΄μš©(idle νŒμ • 끔, blocked μ•Œλ¦Ό μœ μ§€)도 κ°™λ‹€. - μƒˆ `test_s13_the_unreliable_case_needs_no_switch_in_the_product`(`MAM_TEST_` λ¬Έμžμ—΄ λΆ€μž¬)κ°€ base μ—μ„œ μ‹€νŒ¨ν•œλ‹€. ## F6 β€” λ¬Έμ„œΒ·ν…ŒμŠ€νŠΈ 정밀도 - delegate-job SKILL.md:53 은 이제 "≀ 60 s λŠ” herdr wait ν•œμ •, μ•Œλ¦Ό **λ°œν–‰ 쀑**에 λž˜νΌκ°€ 사라지면 κ·Έ λ°œν–‰μ΄ 끝날 λ•ŒκΉŒμ§€" 라고 μ λŠ”λ‹€ β€” μ½”λ“œ(`_wrapper_gone` 이 wait 직후와 `_may_prompt` μ—λ§Œ 있음)와 μΌμΉ˜ν•œλ‹€. - "λŒ€κΈ° 쀑 래퍼 μ†Œλ©Έ" ν…ŒμŠ€νŠΈλŠ” 이제 **두 번째 wait λŒ€μ—­ μ•ˆμ—μ„œ** `gone` 을 μ„Έμš°κ³ , `ticks == [1, 2]` 와 λ§ˆμ§€λ§‰ alive 관츑이 `(2, False)` μž„μ„ λ‹¨μ–Έν•œλ‹€. 이름 κ·ΈλŒ€λ‘œλ₯Ό κ°•μ œν•œλ‹€. - ν…ŒμŠ€νŠΈ 정밀화라 base μ œν’ˆ μ½”λ“œμ—μ„œλ„ ν†΅κ³Όν•˜λŠ” 것이 λ§žλ‹€(μ œν’ˆ λ™μž‘μ€ PR #41 μ—μ„œ 이미 μ˜³μ•˜λ‹€). ## 7. ν…ŒμŠ€νŠΈ ### 7-1. 검증 κ²½λ‘œμ—μ„œμ˜ μ‹€ν–‰ ``` 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_issue5_phase1_delegation.py (.mam.env λ―Έexport, MQTT_* λ―Έμ„€μ •, followups .venv) β†’ 335 passed, 1 skipped in 451.38s failed 0 (skip = S6, /proc) ``` λΈŒλ¦¬ν”„ λͺ©λ‘ 7νŒŒμΌμ— `test_issue5_phase1_delegation.py`(registry CLI μŠ€λƒ…μƒ·Β·help 캑처)λ₯Ό 더해, μ§„μž…μ  변경이 CLI 계약을 κΉ¨μ§€ μ•ŠμŒμ„ ν™•μΈν–ˆλ‹€. λ‘œκ·Έμ— μ™ΈλΆ€ 브둜컀 흔적 0건. ### 7-2. μˆ˜μ • μ—†μ΄λŠ” μ‹€νŒ¨ν•˜λŠ”κ°€ β€” 확인함 검증 경둜 μ‚¬λ³Έμ—μ„œ `tests/` λ°–μ˜ 바뀐 파일 μ „λΆ€λ₯Ό `0051fd6` 판으둜 되돌리고(μƒˆ `lib_int.sh` λŠ” μ‚­μ œ) μƒˆΒ·λ³€κ²½ ν…ŒμŠ€νŠΈλ₯Ό λŒλ Έλ‹€: ``` 47 failed, 88 passed in 110.20s ``` | ν•­λͺ© | base μ—μ„œ μ‹€νŒ¨ν•œ νšŒκ·€ | |---|---| | F2 | `test_a1_python_entry_points_report_an_unreadable_file_in_one_line` 14건(7 μ§„μž…μ  Γ— κΆŒν•œ/UTF-8) | | F3 | `test_a7_a_leading_bom_…`, `…_repeated_key_keeps_its_first_value…`, `…_missing_explicit_file_…` | | F4 | `test_b1_runner_wait_grace/child_wait/job_tree_max_*`, `test_b5_*`(ν‘œΒ·μ΅œμ†Ÿκ°’Β·lib.sh 없이 source λ˜λŠ” 두 슀크립트·`_pane_quiescent`) | | F5 | `test_s13_the_unreliable_case_needs_no_switch_in_the_product` | | F1Β·F6 | ν…ŒμŠ€νŠΈλ§Œ 고친 ν•­λͺ©μ΄λΌ base μ œν’ˆ μ½”λ“œμ—μ„œ ν†΅κ³Όν•˜λŠ” 것이 정상. F1 은 Β§F1 의 `ps` λŒ€μ—­ μ‹€ν—˜μœΌλ‘œ μ˜› ν…ŒμŠ€νŠΈμ˜ μ‹€νŒ¨ 원인을 직접 μž¬ν˜„ | ### 7-3. 격리 μ‹€μ œ `.mam/jobs` λŠ” 이 작 μžμ‹ μ˜ 파일 μ™Έ **μΆ”κ°€ 0**. 검증 경둜 tracked 파일 λ³€ν™” μ—†μŒ(HEAD `95eabc6`), λ³Έ 체크아웃 `main 0051fd6`. ## 8. λ²”μœ„ 바뀐 파일 27개λ₯Ό λͺ¨λ‘ F1–F6 λ˜λŠ” κ·Έ ν•„μˆ˜ λ™λ°˜ λ³€κ²½μœΌλ‘œ 귀속할 수 μžˆλ‹€. - `lib_int.sh` 뢄리와 그에 λ”°λ₯Έ `deploy/remove.sh`Β·`test_issue21_loop_b.py` 의 ν•œ 쀄은 F4b 의 λ™λ°˜ 변경이닀. - #17 ν•­λͺ© 4Β·#39Β·#40Β·#42 λ²”μœ„μ˜ λ³€κ²½κ³Ό 버전 bump λŠ” μ—†λ‹€. ## 9. νŒμ • | # | 비차단 κ΄€μ°° | κ·Όκ±° | |---|---|---| | T1 | `test_o2_race_free_lock.py:get_lstart` λŠ” μ •κ·œν™”ν•˜μ§€ μ•ŠλŠ” **ν˜ΈμΆœλ˜μ§€ μ•ŠλŠ”** 헬퍼 β€” μ§€μš°κ±°λ‚˜ 같은 μ •κ·œν™”λ‘œ λ§žμΆ”λ©΄ 재발 μ—¬μ§€κ°€ μ—†λ‹€ | `tests/test_o2_race_free_lock.py:40-43` | | T2 | reconcile.sh 의 λ‚΄μž₯ κ΅¬λ…μžλŠ” `ensure_env_loaded()` λ₯Ό 직접 뢈러 읽기 μ‹€νŒ¨ μ‹œ μ—¬μ „νžˆ 트레이슀백 | λΈŒλ¦¬ν”„ μ§„μž…μ  λͺ©λ‘ λ°–; λ‹€μŒ 정리 λ•Œ `load_env_or_report` 둜 | | T3 | (확인 μ™„λ£Œ, 쑰치 λΆˆν•„μš”) `deploy/install.sh` λŠ” 트리 전체λ₯Ό λ³΅μ‚¬ν•˜λ―€λ‘œ(`cp -R "$REPO_URL/."`, :128) `lib_int.sh` 도 ν•¨κ»˜ λ°°ν¬λœλ‹€. `check_assets_present`(:98-108)λŠ” 쑴재 ν™•μΈμš© λͺ©λ‘μ΄λΌ `lib.sh` 만 μžˆμ–΄λ„ λ˜μ§€λ§Œ, lib.sh κ°€ lib_int.sh λ₯Ό source ν•˜λ―€λ‘œ λͺ©λ‘μ— μΆ”κ°€ν•΄ 두면 λΆ€λΆ„ μ„€μΉ˜λ₯Ό 더 일찍 μž‘λŠ”λ‹€ | `deploy/install.sh:98-108` | [VERDICT: PASS]