# B-1 구현 계획서 **Rev.2** — `find_workspace_uuid` tier-3 신원 캐시 복구

- **Job**: `662f07ba` (Planner) — 원안 `64990742` Rev.1 을 Creator 이의제기 `2b9e9b67`(agy) 반영하여 개정
- **대상**: `IMPROVEMENTS.md` §2 / **B-1**
- **기준 커밋**: `b6c41e6` (작업 트리 clean)
- **작성 원칙**: 이의제기의 **전제와 처방을 각각 따로 실행하여** 판정한다. 상대 코드를 그대로 돌려 보지 않고 논평하지 않는다.

---

## 0. 이의제기 판정 요약 (Adjudication)

| 대상 | 판정 | 근거 (실측) |
|---|---|---|
| **전제** — "`d`가 truthy인데 `agent_identities`가 없고, YAML에는 남아 있는 상태에서 폴백이 영구 차단된다" | **기각 (Refuted)** | `load_state_json`과 폴백은 **같은 base 경로 · 같은 DB-우선 우선순위**로 읽는다. 6가지 디스크 상태 전수 측정 결과 네 변형이 **전 셀 동일** |
| **처방** — `if isinstance(d, dict) and d.get('agent_identities'):` 로 바꾸면 하위호환 신원이 "완벽히 복원"된다 | **기각 (Refuted)** | 제안 코드를 **원문 그대로 실행**했다. 문제로 지목된 **S2·S6 셀에서 여전히 miss**. 폴백에는 진입하지만(probe=1) 폴백 역시 DB를 먼저 읽어 YAML에 도달하지 못한다 |
| **처방의 경로 폴백** — `os.environ.get('YAML_PATH','')` → 없으면 `<ws>/.mam/...` 추측 | **거부 (Rejected)** | 본 계획서가 제거하려는 **D1(경로 추측)을 그대로 재도입**한다. 직전 잡 `4dbf4feb`의 `${1:-$PWD}`와 동일한 패턴 |
| **조건 변경 자체** | **채택 (Adopted — 단, 근거는 다름)** | 하위호환 복구 목적으로는 무의미하나, **`load_state_json`의 반환 형태 변경에 대한 내성**으로는 유효. 재조회 비용 실측 **0.063 ms** → 성능을 반대 논거로 쓰지 않는다 |
| 원안의 나머지 (D1·D3·D4·D5, V-1…V-6, 커밋 분할) | **유지** | 이의 없음 |

> **요약**: agy가 지적한 셀(S2/S6)은 **agy의 코드로도 고쳐지지 않는다**. 그 셀을 실제로 고치려면 **DB(권위)를 제치고 YAML(미러)을 읽어야** 하는데, 이는 `atomic_dump_yaml`의 설계와 정면으로 충돌한다. 따라서 Rev.2는 **조건 변경은 받되(근거 교체), 경로 추측은 거부하고, S2/S6를 "의도된 동작"으로 테스트에 못박는다**(V-8).

---

## 1. 전제 검증 — `d`와 폴백은 같은 것을 읽는다

`load_state_json`(`lib.sh:510-545`)의 소스 결정 로직:

```python
yaml_path = os.environ['YAML_PATH']
db_path   = os.path.splitext(yaml_path)[0] + '.db'
if os.path.exists(db_path):        d = json.loads(<state row>) ; d['herdr_sessions'] = <sessions table>
elif os.path.exists(yaml_path):    d = yaml.safe_load(f)
```

Rev.1 폴백과 **완전히 동일한 base 경로, 동일한 DB-우선 우선순위**다. 따라서
`d`가 신원을 못 가졌다 ⟹ 폴백도 같은 자리에서 같은 것을 읽어 못 가진다.
이의제기가 상정한 "`d`에는 없고 디스크에는 있는" 상태가 성립하려면 두 읽기의 **소스가 갈려야** 하는데
갈릴 수 없다.

### 1-1. 6가지 디스크 상태 전수 측정

tier-1(관계없는 세션 1행)·tier-2(`last_conversations.json` 부재)를 굶겨 **tier-3만** 남긴 상태에서
네 변형을 각각 실행:

| 상태 | 디스크 배치 | HEAD | Rev.1 `if d:` | **agy 제안** | Rev.2 |
|---|---|---|---|---|---|
| S1 | DB=신원 있음, YAML=신원 있음 | HIT | HIT | HIT | HIT |
| **S2** | **DB=신원 없음, YAML=신원 있음** ← *이의제기 시나리오* | miss | miss | **miss** | miss |
| S3 | YAML만 존재, 신원 있음 | HIT | HIT | HIT | HIT |
| S4 | YAML만 존재, 신원 없음 | miss | miss | miss | miss |
| S5 | DB만 존재, 신원 없음 | miss | miss | miss | miss |
| **S6** | **DB에 state 행 없음 + sessions 행 존재, YAML=신원 있음** | miss | miss | **miss** | miss |

**네 변형이 6/6 셀에서 동일하다.** 조건 변경은 해석 결과를 단 한 셀도 바꾸지 못한다.

S3가 HIT인 점에 주목할 것 — "YAML에만 신원이 있는" 정상 상태는 `load_state_json`이 `elif` 분기로
YAML 전체를 싣기 때문에 **`d`가 이미 신원을 갖는다**. 이의제기가 걱정한 하위호환 경로는
Rev.1에서 이미 살아 있었다.

---

## 2. 처방 검증 — 제안 코드를 그대로 실행했다

이의제기 §3의 코드를 **한 글자도 바꾸지 않고**(폴백 진입 여부를 세는 `PROBE` 한 줄만 추가) 위 매트릭스에
투입했다. S2 결과:

```
S2   agy=miss   probes=1
```

`probes=1`은 **폴백에 진입했다**는 뜻이다. 그런데도 miss인 이유는 폴백의 첫 분기가
`if os.path.exists(db_path):` 이기 때문이다. DB가 존재하므로 YAML `elif`는 평가조차 되지 않고,
DB의 state 행에는 신원이 없으므로 `ai = {}`로 끝난다. **폴백이 열렸을 뿐 도달하지 못한다.**

S6도 같다 — DB는 존재하고 state 행만 없어 `row = None` → `ai = {}`.

> 이의제기 §2-2의 결론 *"tier-3 캐시 조회를 100% 영구 차단"* 은 **Rev.1이 아니라 두 변형 모두에**
> 해당하며, 제안된 수정은 그 차단을 해제하지 못한다. 전제와 처방이 함께 성립하지 않는다.

### 2-1. S2/S6를 진짜로 고치려면 무엇이 필요한가 (그리고 왜 하지 않는가)

DB에 없고 YAML에만 있는 신원을 살리려면 **DB 조회가 비었을 때 YAML로 넘어가는 union 방식**이어야 한다
(`if ... elif ...`가 아니라 `if not ai and os.path.exists(yaml_path): ...`).

**그렇게 하지 않는다.** `atomic_dump_yaml`(`lib.sh:682-920`)은 SQLite `BEGIN IMMEDIATE` 트랜잭션을
권위로 삼고 YAML은 그 뒤에 temp+rename으로 쓰는 **미러**다. DB에서 사라진 신원을 YAML에서 되살리면:

- `stop_session.sh --purge-conversation`이 DB에서 지운 신원이 **YAML 미러를 통해 부활**한다.
  (`stop_session.sh:333-348`이 지우는 대상이 바로 이 캐시다.)
- 두 파일이 갈린 상태 = 이미 손상된 상태이며, 손상된 미러를 권위보다 우선시키는 복구는
  **P0-C 워크스페이스 격리 보증(다른 워크스페이스 UUID 절대 반환 금지)** 을 무너뜨릴 수 있다.

따라서 S2/S6의 miss는 **버그가 아니라 의도된 동작**이며, Rev.2는 이를 **V-8로 못박아** 후대의
"친절한 복구" 시도를 차단한다.

---

## 3. 처방의 경로 폴백 거부

```python
yaml_path = os.environ.get('YAML_PATH', '')
if not yaml_path:
    yaml_path = os.path.join(ws, ".mam", "agent-sessions.yaml")   # ← D1 재도입
```

`YAML_PATH`는 `env_python`(`lib.sh:661-662`)이 **무조건** 설정한다. 이 블록은 `env_python`을 통해서만
실행되므로 이 가드는 **도달 불가능한 죽은 코드**다. 그리고 만에 하나 도달한다면(= 이 블록이
`env_python` 밖에서 실행되는 미래) 그때야말로 워크스페이스 기준 추측이 **가장 위험**하다 —
`AGENT_SESSIONS_YAML`이 재정의된 배치에서 조용히 엉뚱한 파일을 읽는다. 이는 본 계획서가
제거하려는 D1 그 자체이며, `ea36e81`의 `if 'mam_dir' in locals()`와 **구조가 동일한 죽은 가드**다.

**대신 채택하는 하드닝**: 경로 해석을 `try` **안쪽**으로 옮긴다. `os.environ['YAML_PATH']`를 그대로
쓰되(추측 없음), 만약 미설정이면 `KeyError`가 `except`에 잡혀 `WARN` 한 줄과 `ai={}`로 수렴하여
**`rc=0` 계약이 유지된다**. Rev.1은 이 줄이 `try` 밖에 있어 `KeyError` 시 `rc=1`이 될 수 있었다 —
이의제기가 간접적으로 드러낸 실질적 개선점이며, Rev.2에 반영했다.

(이 경로는 공개 인터페이스로는 재현 불가하다. `env_python`이 항상 변수를 넣기 때문이다.
따라서 **테스트를 붙이지 않고** 구조적 방어로만 남긴다 — 검증 불가한 것을 검증했다고 적지 않는다.)

---

## 4. 조건 변경 채택 — 근거를 교체하여

해석 결과가 6/6 동일하므로 **하위호환 복구를 근거로 한 채택은 불가**하다. 그럼에도 조건 자체는
채택한다. 근거는 하나뿐이다:

> `if d:`는 "**`load_state_json`이 병합 상태에 `agent_identities`를 실어 준다**"는 **암묵적 결합**에
> 의존한다. 이 저장소는 이미 state 블롭에서 `sessions` 테이블을 분리한 전례가 있고(현재 라이브 DB의
> `state.data` 최상위 키는 `['snapshot']` 뿐이다), 같은 방식으로 신원이 분리되면 `if d:`는
> **조용히** 폴백을 건너뛴다.

비용은 실측했다: 재조회 1회 = `sqlite3.connect` + `SELECT` = **0.063 ms/call** (200회 평균, 로컬 SSD).
tier-3은 tier-1·tier-2가 모두 실패했을 때만 도달하는 경로이므로 **성능은 반대 논거가 되지 못한다.**
성능을 이유로 거부하지 않는다는 점을 명시한다.

다만 조건 변경만으로는 위 결합을 **감지**하지 못하고 **은폐**할 뿐이므로(폴백이 조용히 대신 일한다),
**V-7 결합 테스트를 함께 넣는다**. 런타임 우회 + CI 감지 두 겹이 되어야 실제 방어가 된다.

**N-1과의 상호작용(중요)**: 저장소 어디에도 `agent_identities` **쓰기 코드가 없으므로**
(§5 참조) 실사용에서 `d.get('agent_identities')`는 사실상 항상 falsy다. 즉 조건 변경 후
**폴백이 사실상 상시 경로가 된다**. 이는 무해하지만, "1차 경로는 재조회 없음"이라던 Rev.1의
설계 근거 ②는 **더 이상 성립하지 않는다**. D2의 의의는 "재조회 제거"가 아니라
**"경로 추측 제거"** 로 축소 기술한다.

---

## 5. HEAD 실측 (Rev.1에서 유지)

### 5-1. 현재 코드 (`.agents/skills/lib.sh:1266-1282`)

```python
ai = {}
db_path = f"{mam_dir}/agent-sessions.db" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.db")
yaml_path = f"{mam_dir}/agent-sessions.yaml" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.yaml")
try:
    import yaml
    if os.path.exists(db_path):
        ...
    elif os.path.exists(yaml_path):
        with open(yaml_path) as f:
            d = yaml.safe_load(f) or {}     # ← 상태 딕셔너리 d 를 덮어씀
        ai = d.get('agent_identities', {})
except Exception as e:
    print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
```

`git blame`: 1267·1268·1270·1281·1282 행이 `ea36e81`(2026-08-05), 나머지는 2026-06-21 원본.
**B-1 원문의 `NameError`는 `ea36e81`에서 이미 제거되었다** — 전제는 낡았고, 증상 대신 경로 추측이 남았다.

### 5-2. 잔존 결함 재현 매트릭스

| # | 조건 | HEAD | Rev.2 |
|---|---|---|---|
| P1 | 상태 파일이 `<ws>/.mam/`에 있음 (기본 배치) | `<uuid>` ✅ | `<uuid>` ✅ |
| **P2** | `AGENT_SESSIONS_YAML`이 `<ws>/.mam/` **밖** | **`""` 무음** ❌ | `<uuid>` ✅ |
| **P3** | `.db` 존재 + PyYAML 없음 | **`""`** ❌ | `<uuid>` ✅ |
| P4 | `agent_identities` 없음 | `""` ✅ | `""` ✅ |
| P5 | `project_cwd` 불일치 | `""` ✅ | `""` ✅ |
| **P7** | `agent_identities`가 dict 아님 | **Traceback, `rc=1`** ❌ | `""`, `rc=0` ✅ |
| **P8** | hermes 신원이 `conversation_id` 키 | **`""`** ❌ | `<uuid>` ✅ |

---

## 6. 결함 목록

### **D1 — 상태 파일 경로를 추측한다 (핵심)**
`env_python`이 `YAML_PATH`로 권위 경로를 넘겨주고(`lib.sh:661-662`), 같은 파일의 다른 두 블록은
이를 지킨다(`load_state_json` L512, `atomic_dump_yaml` L746). tier-3만 `os.path.join(ws, ".mam", ...)`를
직접 조립한다. `AGENT_SESSIONS_YAML`은 `lib.sh:26`에서 재정의 가능하고 해석 대상 워크스페이스는
`WORKSPACE_ROOT`와 같을 의무가 없다 → 갈리는 순간 무음 실패(P2).
`if 'mam_dir' in locals()` 가드는 `mam_dir` 정의가 0건이므로 **항상 False**인 죽은 코드다.

### **D2 — 이미 로드한 상태를 다시 읽는다** *(Rev.2에서 축소 기술)*
`find_workspace_uuid`는 진입 시 `MAM_STATE_JSON`으로 병합 상태를 받아 `d`에 담는다(L1130-1133).
tier-3의 두 번째 읽기는 이 중복이 D1·D3·D4를 낳은 **구조적 원인**이다. 단, §4에 따라 Rev.2는
재조회 자체를 제거하지 않는다 — **경로 추측만 제거**한다.

### **D3 — `import yaml`이 SQLite 분기까지 죽인다**
`try` 첫 줄의 `import yaml` 때문에 PyYAML 부재 시 YAML이 필요 없는 `.db` 분기까지 무력화된다(P3).
**도달 가능성**: `deploy/install.sh:86`이 *시스템* python3의 PyYAML을 하드 게이트하지만
`_delegate_py_bin`(L1416-1421)은 **`$VIRTUAL_ENV`를 최우선** 선택한다. 무관한 venv 활성 상태에서
게이트를 우회한다 — stub 없이 실제 bare venv로 실측:
```
picked python: .../barevenv/bin/python
HEAD out=<Traceback ... ModuleNotFoundError: No module named 'yaml'|WARN: tier-3 identity lookup failed: ...|>
```

### **D4 — `d` 섀도잉 + `rc=0` 계약 위반**
YAML 분기의 `d = yaml.safe_load(f)`가 tier-1/2의 상태 딕셔너리를 덮어쓴다(현재는 이후 미사용이라
무해하나, 한 줄만 추가돼도 즉시 오동작하는 함정). 더 심각한 것은 `ai.get(agent)`(L1284)가 `try`
**밖**이라 비-매핑 입력 시 `AttributeError` → `rc=1`이 된다는 점이다(P7). 함수 주석의
**"Always exits 0"**(L1106) 위반이며, `resolve_session_id.sh:44`는 `set -euo pipefail` 아래
**마지막 명령**이라 그대로 스크립트 종료 코드가 된다(`stop_session.sh:140,159`는 `|| true`로 방어됨).

### **D5 — hermes/cline 폴백이 잘못된 딕셔너리를 본다**
```python
cand = ai_agent.get('session_id') or ai.get('conversation_id')   # L1295, L1299
```
`ai`는 `{agent: {...}}` 맵이므로 `ai.get('conversation_id')`는 **항상 None**. cline은 tier-2 디스크
스캔에 가려지지만, hermes는 tier-2가 `WHERE cwd=?`로 조회하므로 hermes DB의 cwd가 다르면
tier-2가 실패하고 tier-3만 남는다 → 폴백이 죽는다(P8).

---

## 7. 인접 발견 (B-1 범위 밖 — 별도 등재)

### **N-1 — `agent_identities` 쓰기 코드가 저장소에 0건**
전수 조사: `lib.sh:1275,1280`(읽기), `stop_session.sh:334`(기존 항목 비우기),
`reconcile.sh:690`(진단 읽기). **생성·갱신 0건**, `git log -S"d['agent_identities']"`도 공집합.
라이브 워크스페이스의 `state.data` 최상위 키는 `['snapshot']`뿐이고 `agent_identities`는 `null`이다.
→ B-1을 완벽히 고쳐도 **현행 코드로 생성된 워크스페이스의 복원 건수는 0**이다. tier-3은
(a) 마이그레이션된 YAML, (b) 수동 편집 상태를 위한 **하위호환 읽기 경로**로만 의미를 갖는다.
§4에서 본 것처럼 이 사실은 조건 변경의 실효(폴백이 상시 경로가 됨)에도 직결된다.

### **N-2 — `load_state_json`의 PyYAML 하드 의존**
`lib.sh:511`이 모듈 최상단에서 `yaml`을 import 하여, `.db`만으로 충분한 경우에도 PyYAML 부재 시
traceback과 함께 `d={}`가 되어 **tier-1·tier-2가 함께 붕괴**한다(P3 stderr에서 관측).

---

## 8. 왜 기존 55개 테스트가 이를 잡지 못했는가

`tests/conftest.py::mam_sandbox`는 `AGENT_SESSIONS_YAML = tmp_path/.mam/agent-sessions.yaml`,
`WORKSPACE_ROOT = tmp_path`로 두고, 테스트는 `find_workspace_uuid {tmp_path} ...`로 호출한다.
따라서 추측 경로 `<ws>/.mam/agent-sessions.yaml`과 권위 경로가 **모든 테스트에서 문자 단위로 동일**하다.
하네스가 구조적으로 D1을 볼 수 없다.
→ 신규 테스트는 **워크스페이스 디렉터리와 상태 파일 디렉터리를 분리한 `split_sandbox`**를 쓴다.

---

## 9. 구현 계획

> 원자적·이등분 가능 커밋 4개. F1↔F2↔F3 사이 순서 제약 없음(각 테스트가 서로 독립적으로 red).

### **F1 — tier-3 상태 소스 교정** (D1·D3·D4 해소, D2 축소) — **Rev.2 개정**

`lib.sh:1266-1282` 전체를 아래로 치환한다. (프로토타입 검증 완료)

```python
ai = d.get('agent_identities') if isinstance(d, dict) else None
if not isinstance(ai, dict) or not ai:
    ai = {}
    try:
        yaml_path = os.environ['YAML_PATH']
        db_path = os.path.splitext(yaml_path)[0] + '.db'
        if os.path.exists(db_path):
            conn = sqlite3.connect(db_path, timeout=60.0)
            row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
            if row:
                ai = json.loads(row[0]).get('agent_identities') or {}
            conn.close()
        elif os.path.exists(yaml_path):
            import yaml
            with open(yaml_path) as f:
                _ydoc = yaml.safe_load(f) or {}
            ai = _ydoc.get('agent_identities') or {}
    except Exception as e:
        print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
if not isinstance(ai, dict):
    ai = {}
```

**Rev.1 → Rev.2 변경 2건:**

| # | 변경 | 이유 |
|---|---|---|
| ① | `if d:` → `d`에서 **신원을 실제로 확보했는지**로 분기 | 이의제기 채택. 단 근거는 하위호환이 아니라 **`load_state_json` 반환 형태 변경 내성**(§4). V-7과 한 쌍으로만 유효 |
| ② | `yaml_path`/`db_path` 해석을 **`try` 안쪽**으로 이동 | `YAML_PATH` 미설정 시 `KeyError`가 `rc=1`이 되는 잔여 계약 위반 차단(§3) |

**거부한 변경 1건**: `os.environ.get('YAML_PATH','')` → `<ws>/.mam/...` 추측 폴백. D1 재도입(§3).

설계 근거:
1. **1차 소스는 이미 로드된 `d`** — 정상 경로에서 재조회 없음.
2. **폴백은 `YAML_PATH` 기반, 추측 없음** — L512/L746과 동일 규약. `.db` 파생도 동일.
3. **`import yaml`은 YAML 분기 안** — SQLite 분기가 PyYAML에 인질 잡히지 않는다.
4. **`_ydoc` + 이중 `isinstance` 가드** — 섀도잉 제거, `rc=0` 계약 복원.

### **F2 — hermes/cline 폴백 키 수정** (D5)

```diff
-        cand = ai_agent.get('session_id') or ai.get('conversation_id')
+        cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
```
`lib.sh:1295`, `lib.sh:1299` 두 곳. **`replace_all` 금지** — 두 줄이 완전히 동일하므로
문맥(`elif agent == 'hermes':` / `'cline'`)을 포함해 개별 치환할 것.

### **F3 — 회귀 테스트 신설**
신규 파일 `tests/test_b1_tier3_identity.py` (V-1…**V-8**). 전용 `split_sandbox` 픽스처를 파일 내부에
둔다 — `conftest.py::mam_sandbox`를 고치면 기존 55개 테스트의 전제가 흔들리므로 **공용 픽스처는
건드리지 않는다**.

### **F4 — 주석·문서 정합**
- `lib.sh:1105` tier-3 설명에 상태 소스가 `d`(1차)와 `$YAML_PATH`(폴백)임을 명시.
- **DB가 권위, YAML은 미러**이며 tier-3은 미러를 권위보다 앞세우지 않는다는 한 줄을 추가(§2-1).
- `IMPROVEMENTS.md` B-1 항목 교체 + B-10/B-11 신규 등재(§11).

---

## 10. 테스트 계획 (red-before-green 실측 완료)

| ID | 테스트명 | 검증 대상 | HEAD | Rev.2 |
|---|---|---|---|---|
| V-1 | `test_b1_tier3_honours_agent_sessions_yaml_path` | D1 — 워크스페이스≠상태파일 디렉터리 | **FAIL** | PASS |
| V-2 | `test_b1_tier3_db_branch_survives_missing_pyyaml` | D3 — PyYAML 부재 시 SQLite 분기 생존 | **FAIL** | PASS |
| V-3 | `test_b1_tier3_corrupt_identities_still_exits_zero` | D4 — `rc=0` 계약 | **FAIL** | PASS |
| V-4 | `test_b1_tier3_hermes_conversation_id_fallback` | D5 — hermes 폴백 키 | **FAIL** | PASS |
| V-5 | `test_b1_tier3_refuses_foreign_workspace_identity` | P0-C 격리 유지 (가드) | PASS | PASS |
| V-6 | `test_b1_tier3_absent_identities_is_silent` | 무신원 시 무음·무예외 (가드) | PASS | PASS |
| **V-7** | `test_b1_load_state_json_surfaces_agent_identities` | **결합 고정** — `load_state_json`이 신원을 `d`에 실어야 함 | PASS | PASS |
| **V-8** | `test_b1_tier3_does_not_read_yaml_mirror_behind_the_db` | **동작 고정** — stale DB를 YAML 미러로 되살리지 않음 | PASS | PASS |

**V-7·V-8은 이의제기 대응으로 신설**했다. 둘 다 **HEAD에서도 green인 고정(pinning) 테스트**다 —
red를 만드는 것이 목적이 아니라, §1·§2에서 측정으로 확인한 사실을 **코드로 못박아** 다음 리팩터에서
조용히 깨지는 것을 막는 것이 목적이다. V-7이 깨지면 §4의 결합이 끊어진 것이고, V-8이 깨지면
누군가 미러를 권위보다 앞세운 것이다.

4개 변형 전수 실행 결과:

```
HEAD   4 failed, 4 passed in 1.23s
REV1   8 passed in 1.18s      (if d:)
AGY    8 passed in 1.19s      (이의제기 제안 원문)
REV2   8 passed in 1.20s      (채택안)
```

> 세 수정안이 **동일하게 8 passed**라는 사실 자체가 §1의 결론을 다시 확인해 준다 —
> 조건 변경은 관측 가능한 동작을 바꾸지 않는다.

핵심 픽스처(구현자는 이 구조를 그대로 쓸 것):

```python
@pytest.fixture
def split_sandbox(tmp_path):
    """워크스페이스와 상태 파일을 서로 다른 디렉터리에 둔다 — mam_sandbox 의 우연을 깬다."""
    shutil.copytree(os.path.join(REPO, ".agents", "skills"), tmp_path / ".agents" / "skills")
    ws = tmp_path / "ws";           (ws / ".mam").mkdir(parents=True)
    state_dir = tmp_path / "state"; state_dir.mkdir()
    home = tmp_path / "home";       (home / ".claude" / "projects").mkdir(parents=True)
    return {"root": tmp_path, "ws": ws, "yaml": state_dir / "agent-sessions.yaml",
            "home": home, "lib": tmp_path / ".agents" / "skills" / "lib.sh"}
```

V-2의 PyYAML 부재는 스텁 모듈(`PYTHONPATH=<tmp>/noyaml`의 `yaml/__init__.py`에서 `raise ImportError`)로
결정적으로 재현한다 — 실제 venv 생성 없이 CI에서 빠르고 안정적이다.
검증에 사용한 전체 테스트 파일(8케이스)은 저장소 밖 스크래치패드에 그대로 있다:
`/private/tmp/claude-501/.../scratchpad/b1/test_b1_tier3_identity.py`

---

## 11. 검증 게이트

| 게이트 | 명령 | 통과 기준 | 실측 |
|---|---|---|---|
| **G-A** | `bash -n .agents/skills/lib.sh` | 구문 통과 | 통과 |
| **G-B** | `pytest tests/test_b1_tier3_identity.py -q` (수정 **전**) | **4 failed, 4 passed** | 확인 |
| **G-C** | `pytest tests/test_b1_tier3_identity.py -q` (수정 **후**) | **8 passed** | 확인 |
| **G-D** | `pytest tests/test_tier1_unit.py tests/test_tier2_component.py -q` | **55 passed** (회귀 0) | Rev.2 패치 클론에서 **55 passed / 459.81s** |
| **G-E** | `pytest tests/test_tier2_component.py -k "find_workspace_uuid or resume or stop"` | 10 passed | 확인 (13.3s) |

- G-D는 MQTT 왕복 때문에 ~8분 소요된다. 백그라운드 실행 후 회수할 것.
- `tests/test_sanity.py`는 **HEAD 이전부터 45초 이상 행(hang)** 하는 기존 문제로 본 작업 범위 밖이다
  (baseline 아카이브 대조 확인 완료). 게이트에 포함하지 않는다.

---

## 12. `IMPROVEMENTS.md` 갱신 문안

```markdown
### **B-1: `find_workspace_uuid` tier-3 신원 캐시 해석 오류** — ✅ 해소 (F1/F2)
- tier-3이 상태 파일 경로를 `<workspace>/.mam/` 로 추측하던 문제를 제거하고, 이미 로드된 병합 상태(`d`)를
  1차 소스로, `$YAML_PATH`(권위 경로)를 폴백으로 사용하도록 교정.
- PyYAML 부재 시 SQLite 분기까지 무력화되던 `import yaml` 위치, 상태 딕셔너리 `d` 섀도잉,
  비정상 `agent_identities` 입력 시 `rc=1`(계약 위반), hermes/cline 폴백 키 오류를 함께 수정.
- **DB가 권위, YAML은 미러**이며 tier-3은 미러를 권위보다 앞세우지 않는다(V-8로 고정).
- 회귀 테스트 `tests/test_b1_tier3_identity.py` (V-1…V-8) 신설.

### **B-10: `agent_identities` 쓰기 경로 부재**
- 저장소 전체에 `agent_identities` 를 생성·갱신하는 코드가 **0건**이며, 읽기(lib.sh tier-3,
  reconcile.sh 진단)와 삭제(stop_session.sh --purge-conversation)만 존재한다. 현행 코드로 생성된
  워크스페이스에서 tier-3은 구조적으로 빈 값이며, 하위 호환 읽기 경로로만 기능한다.
- 결정 필요: (a) 세션 캡처 시 기록하는 쓰기 경로 신설 / (b) 하위 호환 전용으로 명시하고 문서화.

### **B-11: `load_state_json` 의 PyYAML 하드 의존**
- `lib.sh:511` 이 모듈 최상단에서 `yaml` 을 import 하여, `.db` 만으로 충분한 경우에도 PyYAML 부재 시
  traceback 과 함께 상태가 `{}` 로 붕괴되어 tier-1·tier-2 가 동시에 무력화된다.
- `_delegate_py_bin` 이 `$VIRTUAL_ENV` 를 최우선 선택하므로 `deploy/install.sh` 의 시스템 python3
  PyYAML 게이트를 우회하는 경로가 실재한다(실측).
```

총 추적 건수 18 → 19 (B-1 해소, B-10·B-11 신규).

---

## 13. 리스크

| ID | 리스크 | 평가 | 완화 |
|---|---|---|---|
| RK-A | 폴백 진입 조건 변경으로 tier-3이 느려진다 | **무시 가능** — 재조회 0.063 ms/call, tier-1·2 실패 시에만 도달 | 실측 기재. 성능을 논거로 쓰지 않음 |
| **RK-B** *(개정)* | `d`에 신원이 있는지로 분기하면 **폴백이 상시 경로가 된다**(N-1 때문) | **중** — 동작은 동일하나 "재조회 없음" 설계 근거가 소멸 | D2를 "경로 추측 제거"로 축소 기술. V-7이 결합 파손을 CI에서 검출 |
| RK-C | `isinstance` 가드가 정상 입력을 거른다 | **없음** — dict일 때 무동작 | V-5·V-6 |
| RK-D | F2 치환 시 동일 문자열 2줄을 `replace_all`로 뭉갠다 | 중 | 문맥 포함 개별 치환 강제 |
| RK-E | 수정 후에도 실사용 복원이 늘지 않아 "미해결"로 오판 | **높음** | N-1(B-10) 동반 보고. 효과 범위를 **하위호환 상태를 가진 워크스페이스**로 한정해 커밋 메시지에 명시 |
| **RK-F** *(신규)* | 후대에 "S2/S6도 살리자"며 YAML 미러 union 폴백을 추가한다 | **중** — `--purge-conversation`으로 지운 신원 부활, P0-C 격리 훼손 | **V-8이 즉시 red**. §2-1 근거를 F4 주석으로 코드 옆에 남긴다 |
| **RK-G** *(신규)* | `YAML_PATH` 미설정 하드닝(변경 ②)이 공개 인터페이스로 검증 불가 | 낮음 | 테스트를 만들지 않고 **구조적 방어로만** 남긴다고 명시. 검증하지 않은 것을 검증했다고 적지 않음 |

---

## 14. 작업 순서 체크리스트 (Creator용)

1. `tests/test_b1_tier3_identity.py` 추가 → **G-B(4 failed, 4 passed)** 확인. *red 미확인 시 진행 금지.*
2. F1 적용(`lib.sh:1266-1282` 치환) → G-A, G-C.
3. F2 적용(`lib.sh:1295,1299` 개별 치환) → G-C 재확인.
4. G-E → G-D(백그라운드)로 회귀 0 확인.
5. F4 주석/문서 → `IMPROVEMENTS.md` §12 문안 반영.
6. 커밋 분할:
   - `fix(lib): resolve tier-3 identity source from loaded state and $YAML_PATH (B-1)`
   - `fix(lib): read hermes,cline tier-3 fallback from the agent entry`
   - `test(lib): add B-1 tier-3 regression suite (V-1..V-8)`
   - `docs(improvements): close B-1, open B-10,B-11`

---

## 15. 경계 선언

본 문서는 **설계·리뷰 산출물**이며 저장소 코드는 한 줄도 수정하지 않았다. 이의제기 코드·Rev.1·Rev.2
세 변형과 8개 테스트는 전부 스크래치패드의 `git archive HEAD` 클론 안에서만 실행했고, 작업 트리는
호출 시점과 동일하게 clean이다(`b6c41e6`). herdr 명령 실행 없음, 라이브 세션 미접촉.

`MULTI_AGENT_RULES.md` §1에 따라 **구현은 Creator, 커밋은 GM 소관**이다. **차단 항목 없음.**

Creator께: 이의제기의 **전제와 처방은 측정으로 기각**되었으나, 그 과정에서 **잔여 계약 위반(변경 ②)**
을 발견했고 **V-7·V-8 두 개의 고정 테스트**를 얻었습니다. 지적해 주신 결합(`if d:`의 암묵적 의존)은
실재하며, Rev.2는 이를 **런타임 우회 + CI 검출** 두 겹으로 막습니다.

[VERDICT: PASS]
[AGREEMENT: REACHED]
