# A-4 `BaseAgentAdapter` — 미해결 현황 분석 · 해결 계획 · 교차 리뷰 (Rev.2) - **job_id**: `f2bd7e13` (Rev.1 = `5af41284`) - **역할**: Planner - **반영한 이의제기**: `50fdb719` (agy, `herdr:agy-creator-01`) — `[VERDICT: PASS WITH CHALLENGE]` - **기준 커밋**: `e10db89` (HEAD) - **실측 하네스**: - `.mam/jobs/5af41284/claude-reports/proposed/probe_a4_status.sh` (Rev.1, 유효) - `.mam/jobs/f2bd7e13/claude-reports/proposed/probe_direct_call_contract.sh` (Rev.2 신규, 실행·검증 완료) - **회귀 기준선**: `tier1 + sanity + workspace_scope + uuid_target` = **46 passed** - **저장소 변경**: 없음 --- ## 0. Rev.1 대비 변경 요약 agy 의 3개 주장은 **메커니즘이 전부 사실**이다. 다만 그중 둘은 **영향 범위 서술이 실측과 다르며**, 그 차이가 우선순위를 바꾼다. | 이의제기 항목 | 메커니즘 | 영향 서술 | Rev.2 반영 | |---|---|---|---| | §1 `HOME_DIR` 폴백 → Silent Fail-Close | **SUSTAINED** (재현) | **정정**: 오늘은 도달 불가 — 어댑터가 활성화시키는 잠복 결함 | N0 신설, Phase 1 **선행 조건** | | §2.1 `load_state_json` 서브셸 포크 | **SUSTAINED** (70ms 실측) | **OVERRULED**: watchdog 매 주기가 아니라 `status.sh` 1회 | N7 신설, **P3** | | §2.2 `python -m` 브리지 오버헤드 | **SUSTAINED** (실측 완료) | 절대값 정정 (25ms → 15.0/18.7ms) | 계약 유지, §6.4 개방 항목 종결 | **추가 발견 (agy 가 짚지 않은 것)**: `lib_py` 세 모듈의 `HOME_DIR` 해석 방식이 **서로 다르다.** 한쪽은 조용히 실패하고 다른 쪽은 시끄럽게 실패한다. 따라서 수정은 "`verify_session.py` 한 줄 고치기"가 아니라 **계약 통일**이어야 한다(§1.3). Rev.1 의 결론(A-4 착수, `lib_py/agents/` 채택, G1 선행)은 바뀌지 않는다. --- ## 1. 이의제기 판정 ### 1.1 Primary — `HOME_DIR` 미주입 시 Silent Fail-Close: **SUSTAINED** 재현했다. `HOME_DIR` 없이 직접 호출하면: ``` HOME_DIR present : False home : '' agy 경로 : /.gemini/antigravity-cli/conversations cline 경로 : /.cline/data/sessions verify(agy) : False ``` `verify_session.py:84` 가 `home = home_dir or os.environ.get("HOME_DIR", "")` 이므로 `home` 이 빈 문자열이 되고, 모든 아티팩트 경로가 사용자 홈이 아니라 **시스템 루트**에 조립된다. `os.path.exists()` 가 전부 `False` 를 반환하니 **어떤 세션도 검증을 통과하지 못한다.** agy 의 서술 그대로다. > **인용 정정**: agy 는 line 110 이라 했으나 HEAD 기준 **line 84** 다. 코드는 인용된 것과 동일하다. ### 1.2 정정 — 이 결함은 *오늘* 도달 가능하지 않다 agy 는 이를 P0 로 제시했다. 현 호출자를 전수 확인한 결과 **오늘은 트리거가 없다**: ``` (a) lib.sh 의 HOME_DIR 주입 지점 : env_python(1030) · atomic_dump_yaml(1092) 둘 다 주입 (b) 파이썬에서 직접 import 하는 저장소 코드 : (없음) (c) 테스트의 호출 방식 : 전부 bash -c "source lib.sh && …" 경유 ``` `lib_py` 를 파이썬에서 직접 import 하는 코드는 **`lib_py` 자기 자신(`workspace_uuid.py` → `verify_session.py`)뿐**이고, 그 경로는 이미 `env_python` 안에서 실행되므로 `HOME_DIR` 이 있다. 테스트도 전부 셸을 경유한다. **따라서 정확한 성격은 이렇다: 오늘은 무해하지만, 내 Rev.1 Phase 1(N4/N5, `lib_py/agents/` 어댑터)이 착수되는 순간 활성화되는 잠복 결함이다.** 어댑터는 정의상 파이썬에서 직접 호출되기 때문이다. 이 구분이 중요한 이유는 우선순위 때문이다. Rev.1 의 G1(파사드 빈 문자열)은 **부분 배포만으로 오늘 발생**한다. 이 건은 **내가 코드를 추가해야 발생**한다. 둘을 같은 P0 로 묶으면 순서를 정할 수 없다. 그래서 이 건은 **P0 가 아니라 Phase 1 의 선행 조건(N0)** 으로 배치한다 — 실질적으로는 "어댑터보다 먼저 해야 한다"는 같은 결론이지만, G1 보다 뒤라는 점이 분명해진다. ### 1.3 추가 발견 — 세 모듈의 실패 방식이 다르다 agy 는 `verify_session.py` 만 지적했다. 세 모듈을 전부 보면 계약이 하나가 아니다: ``` verify_session.py get() → 조용한 기본값 84: home = home_dir or os.environ.get("HOME_DIR", "") workspace_uuid.py environ[] → KeyError (시끄러움) 21: home = os.environ['HOME_DIR'] atomic_yaml.py HOME_DIR 미사용 ``` **`workspace_uuid.py` 는 이미 올바르게 행동한다** — 계약이 깨지면 `KeyError` 로 즉시 죽는다. `verify_session.py` 만 조용히 잘못된 답을 낸다. 그러므로 수정 방향은 agy 가 제안한 "`verify_session.py` 한 줄 교체"보다 넓어야 한다: **`lib_py` 전체에 하나의 `HOME_DIR` 해석 계약을 두고 세 모듈이 그것을 쓰게 한다.** agy 가 제안한 폴백 체인 자체는 채택한다: ```python home = home_dir or os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~") ``` `$HOME` 으로 떨어지는 것이 격리 계약을 해치지 않는지 확인했다 — `lib.sh:41` 이 `HOME_DIR="${HOME_DIR:-$HOME}"` 이므로 둘은 기본적으로 같은 값이고, 격리는 `HOME_DIR` 이 아니라 **`iso_root`** 로 수행된다(`f"{iso_root or home}/…"`). 따라서 폴백이 다른 워크스페이스의 저장소를 읽게 만들지 않는다. 다만 **마지막에 빈 값이 남으면 예외를 던져야 한다.** `expanduser("~")` 까지 실패하는 환경(HOME 없는 컨테이너)에서 다시 `""` 로 떨어지면 같은 결함이 재발한다. ### 1.4 §2.1 서브셸 포크 — 메커니즘 SUSTAINED, 영향 OVERRULED 코드는 agy 가 말한 자리에 있고, 비용도 실측했다: ``` reconcile.sh:350 script = f"source '{lib_sh}' && load_state_json" 포크 비용 n=5 median=70 ms (min 68 / max 74) ``` **그러나 이 포크는 watchdog 경로에서 일어나지 않는다.** 해당 블록은 이런 가드 안에 있다: ```python try: d except NameError: …subprocess… ``` 그리고 `atomic_dump_yaml` 은 `d` 를 **미리 정의한다**(`lib_py/atomic_yaml.py:97/101/103). `reconcile.sh:861-865` 의 분기를 보면: ``` DRY_RUN=1 → env_python → d 없음 → bash 포크 (70ms) DRY_RUN=0 → atomic_dump_yaml → d 있음 → 포크 없음 ``` 그리고 실제 소비자는: ``` create_session.sh:389 reconcile.sh --once (쓰기 → 포크 없음) lib.sh:1395 reconcile.sh --subscribe (쓰기 → 포크 없음) status.sh:19 reconcile.sh --dry-run (읽기 → 포크 발생) ``` **즉 백그라운드 watchdog 은 포크하지 않는다.** 포크가 일어나는 유일한 소비자는 `status.sh` — 사용자가 직접 실행하는 상태 조회 명령이다. agy 의 서술("모니터 스위프 시 … 매 주기 발생 … 백그라운드 watchdog의 응답 지연과 CPU 자원 낭비")은 성립하지 않는다. 실제 성격은 **대화형 `status.sh` 1회당 70ms** 이다. 고칠 가치는 있으나(대화형 명령에서 70ms 는 체감된다) **P3 이며, A-4 의 선행 조건이 아니다.** ### 1.5 §2.2 브리지 오버헤드 — SUSTAINED, 절대값 정정 Rev.1 §6.4 에서 "재측정하지 않았다"고 남긴 개방 항목을 이번에 닫았다: ``` python -m (venv) : 15.0 ms python -m (system) : 18.7 ms bash case (기준선) : 2.8 ms ``` 설계 문서의 22.8ms 와 agy 의 ~25ms 는 이 환경에서 **다소 비관적**이다(15.0/18.7ms). 그러나 **bash `case` 대비 5.4~6.7배**라는 관계는 그대로이므로, **"스크립트당 1회 호출 후 `eval`" 계약은 변경 없이 유효하다.** 분기마다 호출하면 안 된다는 결론이 절대값이 아니라 배수에서 나오기 때문이다. --- ## 2. 현황 (Rev.1 에서 변경 없음 — 요약) `e10db89` 이후 상태와 미해결 3건(G1/G2/G3)은 이의제기의 영향을 받지 않았다. 전문은 `5af41284` 보고서를 참조하고 결론만 옮긴다. - **G1 (P0)**: `VERIFY_SESSION_PYTHON` 파사드가 파일 유실 시 조용히 빈 문자열 → `reconcile.sh` 만 `NameError` 로 죽음. **오늘 도달 가능.** - **G2**: 팬아웃 39 → 44 (**+5**). 증가분이 전부 정상적인 결함 수정(F4/F6)에서 나왔다 — 추상화 없이 고칠수록 사본이 는다. - **G3**: A-4 M0 체크리스트가 `mam_agents` 가 아니라 `lib_py` 에 대해 이미 충족. `lib_py/agents/` 채택 권고, CI `py_compile` 재귀 교정 필수. **신규 (이의제기 반영):** - **G4**: `lib_py` 의 `HOME_DIR` 해석 계약이 모듈마다 다르고, `verify_session.py` 는 조용히 실패한다. **Phase 1 선행 조건.** - **G5**: `status.sh --dry-run` 경로에 70ms 서브셸 포크. **P3.** --- ## 3. 실행 계획 (Rev.2) ### Phase 0 — 즉시 (A-4 와 독립) **N1. 파사드 폴백을 시끄럽게** — G1, **P0**. 오늘 도달 가능한 유일한 건. **N2. `reconcile.sh` 를 import 로 전환** — `98393a97` D2b-③ 잔여. 완료 시 파사드 삭제 가능(N1 무의미화). **착수 전 §4.3 전수 조사 필요.** **N3. CI `py_compile` 재귀 교정** — G3. 어댑터 도입 **전에** 해야 신규 파일이 처음부터 검사된다. ### Phase 1 — A-4 M0/M1 **N0. `lib_py` `HOME_DIR` 해석 계약 통일** — G4, ← *이의제기 §1 반영, 신규.* **N4 의 선행 조건.** `lib_py/paths.py`(또는 `verify_session.py` 내 공용 헬퍼)에 단일 해석 함수를 두고 세 모듈이 그것만 쓰게 한다: ```python def resolve_home(home_dir=None): h = home_dir or os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~") if not h or h == "/": raise ValueError("HOME_DIR unresolvable — refusing to build paths from the filesystem root") return h ``` `workspace_uuid.py:21` 의 `os.environ['HOME_DIR']` 도 이 함수로 교체한다 — 지금은 우연히 올바르게 시끄럽지만, 계약이 두 벌인 상태를 남기지 않는다. *회귀 테스트*: `env -u HOME_DIR` 로 `verify_session_uuid` 를 직접 호출해 **`False` 가 아니라 정상 동작**하는지, 그리고 `HOME`/`HOME_DIR` 둘 다 없을 때 **`ValueError` 로 죽는지**. 현재 이 경로를 검증하는 테스트는 0개다(§4.2). **N4. `lib_py/agents/` 골격** — `base.py` · `registry.py` · `__main__.py` · `adapters/{claude,agy,hermes,cline}.py`. `mam_agents/` 신설안 폐기. **셸 브리지 계약을 명시한다**(§1.5 실측 근거): 스크립트 진입 시 `eval "$(python -m lib_py.agents facts )"` **1회**. 루프 안 호출 금지. 15.0ms × 분기 수는 bash `case` 2.8ms 대비 즉시 손해다. **N5. M1 `own_key` / `agent_of_row` 이관** — `a4589a4b` W7/W8d 흡수. 우선순위 ①명시인자 →②레지스트리 row →③이름 접미사 →④`pane.cmd` →⑤실패는 실패. **단 `reconcile.sh` 입양 루프에는 ④ 를 적용하지 않는다**(`3aee63cf` §1.2 실측 반증). ### Phase 2 — A-4 M2~M7 설계 문서 단계를 따르되 M2 는 내용이 이미 landing 되었으므로 **이관만** 수행. M4 는 hermes DB 스키마 실측 후 착수(§4.1). ### Phase 3 — 공백 보충 및 성능 **N6. 입양 소유권 마커** — `3aee63cf` W8a/W8b. A-4 에 대응 항목 없음. **N7. `load_state_json` 파이썬 경로 제공** — G5, ← *이의제기 §2.1 반영, 신규.* **P3.** `lib_py/state.py` 에 `load_state_json()` 을 두고 `reconcile.sh` 의 dry-run 폴백이 `bash -c` 대신 이를 직접 부르게 한다. 대상은 `status.sh` 체감 지연 70ms 이며, **watchdog 성능과는 무관하다**(§1.4). 이는 `6481e5b4` 의 "소형 블록 4개는 옮기지 않는다" 결정에 대한 **부분 예외**다 — `load_state_json`(39줄)은 그 결정 당시 "정적 검사 가치가 낮다"는 이유로 제외했으나, 여기서는 **성능**이라는 다른 근거가 생겼다. 근거가 바뀌었으므로 결정도 바뀐다. --- ## 4. 검증 **팬아웃 게이트** (Rev.1 에서 유지): ``` 현재 44 → M1 이후 ≤38 → M2 이후 ≤30 → 최종 상한 약 15 ``` 각 단계마다 `probe_a4_status.sh` 재실행, S1 단조 감소 확인. 회귀는 **46 passed** 기준선. **신규 게이트:** - **N0**: `env -u HOME_DIR` 직접 호출이 정상 동작 + `HOME`/`HOME_DIR` 부재 시 `ValueError`. `probe_direct_call_contract.sh` D1/D3 이 그대로 게이트다. - **N1/N2**: `lib_py/verify_session.py` 를 지운 사본에서 `reconcile.sh --once` 가 **0 아닌 종료코드**. `lib.sh` 단위 테스트로는 잡히지 않는다. - **N4**: 브리지 호출 횟수가 스크립트당 1회인지 — `python -m lib_py.agents` 호출을 세는 정적 검사. - **N7**: `status.sh` 실행 시간 전후 비교(현재 포크분 70ms). --- ## 5. 리스크 및 미측정 항목 **5.1 hermes 미설치 — 변화 없음.** `lib_py/verify_session.py` 의 `SELECT cwd FROM sessions WHERE id=?` 는 `sessions` 테이블에 `cwd` 컬럼이 있다는 **추론**에 근거하며 실행 확인되지 않았다. 컬럼이 없으면 `sqlite3.OperationalError` 가 `except Exception: return False` 에 삼켜져 **모든 hermes 세션이 조용히 검증 실패**한다 — G4 와 정확히 같은 종류의 침묵이다. M4 전에 확인할 것. **5.2 직접 호출 경로를 검증하는 테스트가 0개다.** §1.2 에서 확인했듯 모든 테스트가 셸을 경유한다. 그래서 G4 가 머지될 때까지 아무도 몰랐다. **N0 는 수정과 함께 직접 호출 테스트를 반드시 추가해야 한다** — 그러지 않으면 어댑터 도입 후 같은 종류의 결함이 또 잠복한다. **5.3 N2 착수 전 `reconcile.sh` 의 `lib.sh` 의존 전수 조사가 여전히 미이행이다.** `98393a97` §6.1 → `5af41284` §6.3 → 지금까지 세 번째로 남기는 권고다. `MAM_VERIFY_PY` 는 전수 grep 했으나 다른 셸 변수·함수 의존은 조사하지 않았다. **이번 이의제기(§2.1)가 정확히 그 미조사 영역에서 나왔다** — 조사했다면 내가 먼저 찾았을 것이다. **5.4 §1.5 의 브리지 측정은 최소 스텁 기준이다.** 실제 어댑터는 `registry.py` + 4개 어댑터 모듈을 import 하므로 15.0ms 보다 느려진다. N4 착수 시 **실제 패키지로 재측정**할 것. 배수가 커지면 `eval` 1회 계약이 더 중요해질 뿐 방향은 같다. **5.5 G2 지표는 독립 구현이다.** A-4 문서의 34 와 이 보고서의 44 를 직접 빼서 쓰면 안 된다. **delta(+5)가 근거다.** **5.6 `lib_py/agents/` 채택은 되돌리기 쉽다.** `mam_agents/` 로 되돌리는 비용은 `remove.sh` 1줄 + CI 1줄. 이 결정에서 막히지 말 것. --- ## 6. 판정 이의제기 3건의 메커니즘을 전부 확인해 수용했고, 그중 2건의 영향 서술을 실측으로 정정했다. 정정이 우선순위를 바꾼다 — `HOME_DIR` 건은 P0 가 아니라 **Phase 1 선행 조건**이고, 서브셸 포크는 watchdog 병목이 아니라 **`status.sh` 의 P3 개선**이다. 그리고 agy 가 짚지 않은 **모듈 간 계약 불일치(G4)** 를 추가로 발견해 수정 범위를 넓혔다. **[ADJUDICATION: SUSTAINED]** — `50fdb719` §1 (`HOME_DIR` 폴백), §2.1 (서브셸 포크 존재), §2.2 (브리지 오버헤드) **[ADJUDICATION: OVERRULED]** — `50fdb719` §2.1 의 영향 서술 (watchdog 매 주기 병목) — 쓰기 경로는 포크하지 않음을 실측 **결론: A-4 착수 유지.** 순서는 N1(P0) → N2 → N3 → **N0(신규 선행)** → N4 → N5 → M2~M7 → N6/N7. **[VERDICT: PASS]** *(이 토큰은 이 계획서 산출물의 완성도를 뜻한다. 감사 대상 코드에 대한 판정이 아니다 — 감사 결과는 **P0 1건(G1) 미해결 + 잠복 1건(G4)** 이다.)*