# 📐 구현 계획서 Rev.2 — B-10 (P3-2): `agent_identities` tier-3 신원 캐시 완전 제거 (Option A)

- **Job ID**: `104b94c8` (Rev.1 = `00334786`)
- **Planner**: claude (session: `herdr:canary-projects-multi-agent-mux-creator-claude`)
- **Role**: Planner (`MULTI_AGENT_RULES.md` §1 — 본 작업에서 저장소 코드 0건 수정)
- **반영 대상 Challenge**: `26f5d224` (agy, Worker / Plan Reviewer) — `[VERDICT: PASS WITH CHALLENGE]`
- **기준 커밋**: `7e21077` (`refactor`, 작업 트리 clean)

---

## 0. 요약

**Challenge 2건 모두 타당합니다. 전면 수용합니다.** 격리 클론에서 Rev.1 의 가드 코드를 **원문 그대로 실행**해 두 결함을 재현했습니다.

그리고 챌린저의 권고안 #1 을 실제로 구현해 보는 과정에서 **생산 코드 결함 1건을 새로 발견**했습니다. 이것이 이번 Rev.2 의 가장 중요한 산출입니다.

> **신규 발견**: `verify_session.py:10` 이 `import os, sys, json, sqlite3, yaml` 로 **yaml 을 즉시 import** 합니다. 이 함수(`mam_orchestrator_uuids`)는 `find_workspace_uuid_main()` 이 `:38` 에서 **tier 로직보다 먼저** 호출합니다. 따라서 **tier-3 을 제거해도 UUID 해결 경로는 여전히 PyYAML 을 요구합니다.** 브리프의 목표("remove PyYAML dependency from workspace_uuid.py")는 *파일* 단위로는 달성되지만 *실행 경로* 단위로는 달성되지 않습니다.

이는 B-10 항목 (b) 가 원래 `lib.sh`/`load_state_json` 에 대해 서술했던 **바로 그 결함 패턴이 다른 파일에 미수정 상태로 남아 있던 것**입니다. `state.py:34` 가 이미 올바른 선례(분기 내부 import)를 제공하므로 1줄로 교정됩니다. **단계 4 로 추가했습니다.**

| 항목 | Rev.1 | Rev.2 |
|---|---|---|
| C2 `pathlib` NameError | 존재 | **수정** |
| C1 가드 공허성 | `import lib_py.workspace_uuid` — 베이스라인에서도 통과 | **AST 검사 + 실행 검사 2종으로 교체** |
| 가드 수 | 2 | **3** |
| `verify_session.py:10` 즉시 yaml import | **미인지** | **단계 4 신설** |
| 뮤테이션 검증 | 계획만 제시(M1~M3) | **5종 실측 완료(M1·M2·M3a·M3b·M4)** |
| 제거 단계 자체의 실행 검증 | 미실시 | **클론에 선적용 후 구문·가드·전체 회귀 확인** |

---

## 1. Challenge 판정 — 2건 모두 수용 (실행으로 재현)

### 1.1 C2 — `pathlib` NameError (확인)

Rev.1 §5.1 의 두 번째 테스트는 지역 import 가 `subprocess, sys, os` 뿐인데 `pathlib.Path` 를 씁니다. 첫 번째 테스트가 `pathlib` 을 import 하지만 그것은 **자기 함수 스코프**이고, `tests/test_tier1_unit.py:1-8` 에도 최상위 `import pathlib` 이 없습니다(확인).

Rev.1 가드를 클론에 원문 그대로 붙여 실행:

```
>       skills = str(pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills")
E       NameError: name 'pathlib' is not defined
tests/test_tier1_unit.py:370: NameError
```

챌린저가 예측한 그 줄에서 정확히 재현되었습니다. **단순 누락이며 제 실수입니다.**

### 1.2 C1 — 가드가 베이스라인에서 통과(공허) (확인)

`pathlib` 만 고치고 **tier-3 이 그대로 살아 있는 미수정 베이스라인**에서 다시 실행:

```
tests/test_tier1_unit.py::test_b10_no_agent_identities_reader_in_production  FAILED   ← 정상 (offender 9건 열거)
tests/test_tier1_unit.py::test_b10_workspace_uuid_needs_no_pyyaml            PASSED   ← 공허
1 failed, 1 passed
```

가드 1 은 제 역할을 합니다(제거 전이므로 실패). **가드 2 는 제거가 일어나지 않았는데도 통과**합니다 — 챌린저 지적대로 `import lib_py.workspace_uuid` 는 `find_workspace_uuid_main()` 을 실행하지 않으므로 `:100` 의 지연 import 에 도달하지 못합니다.

**추가 실측 — 공허성의 정확한 범위**: Rev.1 이 제안했던 뮤테이션 M3(최상위 `import yaml` 추가)은 실제로는 잡습니다. 잡지 못하는 것은 **이 저장소에 실제로 존재했던 형태**, 즉 함수 내부 지연 import 입니다.

| 뮤테이션 | Rev.1 가드 2 |
|---|---|
| M3a — 최상위 `import yaml` | **FAIL** ✅ 잡음 |
| M3b — 함수 내부 지연 `import yaml` (C1 이 지목한 형태) | **PASS** ❌ 못 잡음 |

즉 제가 설계한 가드는 **제가 상상한 결함 형태만** 방어하고 **실제로 있었던 형태**는 놓칩니다. 챌린저 지적이 정확합니다.

---

## 2. 챌린저 권고안 평가

챌린저는 두 가지를 권고했습니다.

### 2.1 권고 #2 (AST/텍스트 검사) — 채택, AST 로 정밀화

텍스트 부분 문자열 검사(`"yaml" not in source`)는 `YAML_PATH` 같은 정당한 식별자에 걸려 향후 오탐을 냅니다. **AST 로 `Import`/`ImportFrom` 노드만** 검사하면 중첩 깊이와 무관하게 정확히 잡습니다.

### 2.2 권고 #1 (mock env 로 `find_workspace_uuid_main()` 실행) — 채택, **단 그대로는 오탐**

방향은 옳습니다. 그러나 **명세된 형태로 구현하면 완벽한 B-10 구현 위에서도 실패합니다.** 챌린저가 제시한 4개 환경변수(`WS_ABS`, `AGENT`, `MAM_STATE_JSON`, `YAML_PATH`)를 갖추고 noyaml 스텁 하에서 실행한 결과:

```
AssertionError: resolution path still needs PyYAML:
  File ".../lib_py/workspace_uuid.py", line 38, in find_workspace_uuid_main
    orchestrator_ids = set(mam_orchestrator_uuids())
  File ".../lib_py/verify_session.py", line 10, in mam_orchestrator_uuids
ImportError: PyYAML absent (stub)
```

실패 원인은 `workspace_uuid.py` 가 아니라 **`verify_session.py`** 입니다 — §3 의 신규 발견으로 이어집니다. 권고 #1 은 그 결함을 함께 고친 뒤에야 의미 있는 가드가 됩니다. 이 계획은 **둘 다** 반영합니다.

---

## 3. 🆕 신규 발견 — `verify_session.py:10` 의 즉시 `yaml` import

### 3.1 결함

```python
# verify_session.py:6-11
def mam_orchestrator_uuids():
    global _MAM_ORC_CACHE
    if _MAM_ORC_CACHE is not None:
        return _MAM_ORC_CACHE
    import os, sys, json, sqlite3, yaml      # ← :10  yaml 을 무조건 import
    override = os.environ.get("MAM_ORCHESTRATOR_UUIDS")
```

`yaml` 은 이 함수 안에서 **실제로 쓰입니다** — `:48` 의 `yaml.safe_load(f)` (YAML 폴백). 문제는 **import 위치**입니다. `:10` 은 함수 진입 즉시 실행되므로:

- DB 분기만 타도 PyYAML 필요
- `MAM_ORCHESTRATOR_UUIDS` 환경변수로 조기 반환해도 필요 (import 가 `:10`, 오버라이드 검사가 `:11`)

**실측** — 오버라이드를 빈 문자열로 주어 즉시 반환시켜도:

```
$ PYTHONPATH=<noyaml>:... MAM_ORCHESTRATOR_UUIDS="" python -c "…mam_orchestrator_uuids()"
ImportError: PyYAML absent (stub)
```

### 3.2 왜 B-10 범위인가

`find_workspace_uuid_main()` 은 `:38` 에서 `mam_orchestrator_uuids()` 를 호출합니다 — **tier-1 보다도 먼저**입니다. 따라서 tier-3 을 지워도 UUID 해결 경로 전체는 PyYAML 을 요구한 채 남습니다. 브리프의 목표를 *실행 경로* 기준으로 달성하려면 이 한 줄이 필요합니다.

또한 이것은 B-10 항목 (b) 가 서술한 것과 **동일한 결함 패턴**입니다. (b) 는 `lib.sh`/`load_state_json` 에 대해 제기되었고 `state.py` 이관 과정에서 해소되었는데(Rev.1 §1.2), **같은 패턴이 `verify_session.py` 에 남아 있었습니다.** B-10 을 "PyYAML 의존 완화" 과제로 닫으면서 이걸 남기면 항목이 절반만 닫힙니다.

### 3.3 교정 — `state.py:34` 선례를 그대로 따름

```python
    import os, sys, json, sqlite3                      # :10  — yaml 제거
    ...
            if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p):
                try:
                    import yaml                        # ← YAML 폴백 분기 안으로
                    with open(yaml_p) as f:
                        d_obj = yaml.safe_load(f) or {}
```

**실측 확인**: 이 교정 후 §4 의 실행 가드가 통과합니다(교정 전 FAIL → 교정 후 PASS).

### 3.4 `lib_py` 의 `yaml` import 전수 조사

| 위치 | 판정 |
|---|---|
| `atomic_yaml.py:6` (모듈 최상단) | **정당** — 모듈의 존재 이유가 YAML 직렬화이고, 이중 인터프리터 전략상 시스템 python3(PyYAML 보유)에서만 실행됨 |
| `state.py:34` (분기 내부) | **이미 올바름** — 이번 교정의 선례 |
| `workspace_uuid.py:100` (tier-3 내부) | B-10 단계 1 에서 제거 |
| **`verify_session.py:10` (함수 즉시)** | **단계 4 신설** |

교정 후 `lib_py` 의 무조건적 PyYAML 요구는 `atomic_yaml.py` 하나로 수렴합니다.

---

## 4. 확정 회귀 가드 — 3종, 뮤테이션 5종 실측 완료

### 4.1 확정 코드 — `tests/test_tier1_unit.py` 에 추가

```python
def test_b10_no_agent_identities_reader_in_production():
    """B-10: agent_identities has no writer; no production code may read it."""
    import pathlib
    root = pathlib.Path(__file__).resolve().parent.parent
    targets = [
        root / ".agents" / "skills" / "lib_py" / "workspace_uuid.py",
        root / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh",
        root / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh",
        root / ".agents" / "skills" / "lib.sh",
    ]
    offenders = []
    for f in targets:
        for i, line in enumerate(f.read_text().splitlines(), 1):
            if "agent_identities" not in line:
                continue
            if line.lstrip().startswith("#"):     # 금지 규약을 서술하는 주석은 허용
                continue
            offenders.append(f"{f.name}:{i}: {line.strip()}")
    assert not offenders, "agent_identities read path resurrected:\n" + "\n".join(offenders)


def test_b10_workspace_uuid_has_no_yaml_import():
    """B-10: no `import yaml` anywhere in workspace_uuid.py — top-level OR lazy."""
    import ast, pathlib
    src = (pathlib.Path(__file__).resolve().parent.parent
           / ".agents" / "skills" / "lib_py" / "workspace_uuid.py")
    tree = ast.parse(src.read_text())
    offenders = []
    for node in ast.walk(tree):                       # ast.walk → 중첩 깊이 무관
        if isinstance(node, ast.Import):
            for a in node.names:
                if a.name.split(".")[0] == "yaml":
                    offenders.append(f"line {node.lineno}: import {a.name}")
        elif isinstance(node, ast.ImportFrom):
            if (node.module or "").split(".")[0] == "yaml":
                offenders.append(f"line {node.lineno}: from {node.module} import ...")
    assert not offenders, "PyYAML dependency reintroduced:\n" + "\n".join(offenders)


def test_b10_find_workspace_uuid_runs_without_pyyaml(tmp_path):
    """B-10: the executed resolution path must not need PyYAML."""
    import subprocess, sys, os, json, pathlib
    stub = tmp_path / "noyaml"
    (stub / "yaml").mkdir(parents=True)
    (stub / "yaml" / "__init__.py").write_text('raise ImportError("PyYAML absent (stub)")\n')
    skills = str(pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills")
    ws = tmp_path / "ws"; ws.mkdir()
    env = os.environ.copy()
    env["PYTHONPATH"] = f"{stub}:{skills}"
    env["WS_ABS"] = str(ws)
    env["AGENT"] = "claude"
    env["MAM_STATE_JSON"] = json.dumps({"herdr_sessions": []})
    env["YAML_PATH"] = str(tmp_path / "agent-sessions.yaml")
    env["HOME_DIR"] = str(tmp_path)
    env["CLAUDE_PROJECT_DIR"] = str(tmp_path / "projects")
    r = subprocess.run(
        [sys.executable, "-c",
         "from lib_py.workspace_uuid import find_workspace_uuid_main; find_workspace_uuid_main()"],
        capture_output=True, text=True, env=env)
    assert r.returncode == 0, f"resolution path still needs PyYAML: {r.stderr}"
    assert "yaml" not in r.stderr.lower(), f"PyYAML touched at runtime: {r.stderr}"
```

`env` 를 명시 구성하므로 앰비언트 `PYTHONPATH` 에 의존하지 않습니다(직전 라운드 N1 재발 방지). 지역 import 에 `pathlib` 을 포함시켜 C2 를 해소했습니다.

### 4.2 뮤테이션 매트릭스 — Rev.2 에서 실측

클론에 §5 단계 1~4 를 선적용한 뒤 측정했습니다.

| # | 뮤테이션 | 기대 | 실측 |
|---|---|---|---|
| — | baseline (제거 + 교정 적용) | PASS | **3 passed** ✅ |
| M1 | `workspace_uuid.py` 에 `agent_identities` 읽기 복원 | 가드 1 FAIL | **1 failed** ✅ |
| M2 | `reconcile.sh` 에 drift D 읽기 복원 | 가드 1 FAIL | **1 failed** ✅ |
| M3a | 최상위 `import yaml` | 가드 2 FAIL | **2 failed** ✅ (실행 가드도 동반 실패) |
| **M3b** | **함수 내부 지연 `import yaml`** (C1 형태) | 가드 2 FAIL | **1 failed** ✅ **← Rev.1 이 놓쳤던 형태** |
| M4 | `verify_session.py` yaml 지연 교정 되돌림 | 가드 3 FAIL | **1 failed** ✅ |

M3b 가 Rev.2 의 핵심 개선입니다 — Rev.1 가드에서는 이 뮤테이션이 통과했습니다.

---

## 5. 구현 계획

### 5.1 단계 1 — `workspace_uuid.py` tier-3 제거

`ai = d.get('agent_identities') …` 부터 `print('')` 직전까지 28줄 삭제, import 를 `import os, sys, json` 으로 축소(`sqlite3` 은 tier-3 외 사용처 0건).

> **클론 실측**: 삭제 후 `ast.parse` OK, 전체 회귀 §8-9 참조.

### 5.2 단계 2 — `reconcile.sh` drift D 제거

`# === drift D: stale UUID … ===` 부터 `result = {` 직전까지 35줄 삭제. `bash -n` OK 확인.

**주의**: `glob`/`sqlite3` import 는 **다른 분기에서도 쓰이므로 제거하지 마십시오**(클론 실측에서 삭제 없이 정상 동작).

### 5.3 단계 3 — `stop_session.sh` 캐시 소거 제거

`# agent_identities 는 cache — …` 블록 6줄 삭제. `:164` 주석을 `tier-1(row) -> tier-2(workspace-scoped disk scan)` 로 정정. `bash -n` OK 확인.

### 5.4 🆕 단계 4 — `verify_session.py:10` yaml 지연화 (§3)

```python
-    import os, sys, json, sqlite3, yaml
+    import os, sys, json, sqlite3
```

그리고 `yaml.safe_load` 를 쓰는 YAML 폴백 `try:` 블록 첫 줄에 `import yaml` 을 삽입합니다. **1줄 이동**이며 `state.py:34` 와 동일한 형태입니다.

### 5.5 단계 5 — `lib.sh` 주석 정정

```bash
# Resolution order:
#   1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id
#      (claude_session_id_own / agy_conversation_id_own)
#   2) on-disk scan scoped to this workspace, via the agent adapter's discover()
# Prints the UUID on stdout (empty line if none). Always exits 0.
```

`:1326` 의 `3-tier` → `2-tier`, `… -> cwd-matched cache` 제거.

### 5.6 단계 6 — 스킬 문서

- `status/SKILL.md:108` drift D 행 삭제 (A/B/C 3종만)
- `monitor/SKILL.md:143` 예시 출력의 `agent_identities.*` 줄 삭제
- `resume/SKILL.md:50-58` 해결 순서 교체 — **기존 서술이 이미 오류**입니다. `agent_identities` 를 1·2순위 primary 로 안내하고 있으나 P0-C 가 이를 cache 로 강등했습니다(`update_yaml_resumed.sh:5` 가 명시). 실제 순서로 교체:

```
1. herdr_sessions[] 행의 per-row own id (claude_session_id_own / agy_conversation_id_own)
   — multi-agent-mux-stop 이 종료 직전 확정 기록한 값 (tier-1, race-free)
2. 워크스페이스로 스코프된 온디스크 스캔 (어댑터 discover())
둘 다 비면 → 이 워크스페이스에는 아직 대화가 없음. multi-agent-mux-create 로.
```

**보존**: `resolve_session_id.sh:7` 의 `# P0-C: 전역 agent_identities 를 즉시 반환하지 않는다` 는 **금지 규약** 서술이므로 유지합니다(tier-3 제거로 오히려 더 정확해짐). 가드 1 의 주석 허용 규칙이 이를 통과시킵니다.

---

## 6. `adapter.identity_cache_fields` — Option A 확정

Rev.1 §4 에서 판단을 요청했고 **챌린저가 §3 표에서 "Adopt Option A" 로 동의**했으므로 확정합니다.

단계 3 이 `stop_session.sh` 의 유일한 생산 소비자를 제거하므로, `base.py:57` 에 근거 주석을 **반드시** 남깁니다.

```python
    @property
    def identity_cache_fields(self) -> tuple:
        """agent_identities 캐시의 에이전트별 필드명.
        B-10(Option A)로 캐시 읽기 경로가 제거되어 현재 생산 소비자는 0건이지만,
        캐시 쓰기 경로가 도입되면 즉시 필요한 유일한 스키마 기술이므로 존치한다.
        임의 삭제 금지 — 삭제 시 4개 어댑터에 필드명을 다시 흩뿌려야 한다."""
        raise NotImplementedError
```

근거 없는 미사용 속성은 다음 정리 라운드에서 "쉬운 삭제 대상"으로 오인됩니다 — C-4 가 `_HERDR_SHIM_DIR_PATTERN` 에서 정확히 그 사례였습니다.

---

## 7. 문서 동기화

### 7.1 `IMPROVEMENTS.md` — 7곳

| 행 | 현재 | 변경 후 |
|---|---|---|
| `:3` | 최종 갱신일 `2026-08-17 (…, C-6 완료, 263/263)` | B-10 완료 및 266/266 반영 |
| `:5` | 미해결 **4건** (아키 1, **엣지 3**, 오케 0, 레거시 0) | 미해결 **3건** (아키 1, **엣지 2**, 오케 0, 레거시 0) |
| `:6` | 완료 **21건** | 완료 **22건**, 목록에 `B-10` 추가 |
| `:70` | `## 2. … (Edge-case Bugs — 3건)` | `… (Edge-case Bugs — 2건)` |
| `:79-80` | B-10 항목 | **삭제** (§5 로 이동) |
| `:92` | `## 5. … (Completed Tasks — 21건)` | `… (Completed Tasks — 22건)` |
| `:241` | `\| **P3-2** \| **B-10** \| tier-3 신원 캐시 존치/제거 결정 + PyYAML 의존 완화 \| 중 \| A-4 M2 \|` | `… tier-3 신원 캐시 완전 제거 (Option A) **(✅ 완료 — 전체 266/266 PASS)** \|` |

§5 신규 항목:

```markdown
### **B-10 (P3-2): `agent_identities` tier-3 신원 캐시 완전 제거 (Option A)** — ✅ 완료
- 저장소 전체에 `agent_identities` 쓰기 코드가 0건임을 재확인하고(라이브 `.db` 최상위 키에도 부재),
  구조적으로 히트 불가였던 읽기 경로 3곳을 제거했습니다 — `workspace_uuid.py` tier-3 폴백(28줄),
  `reconcile.sh` drift D 진단(35줄), `stop_session.sh` purge 시 캐시 소거(6줄), 관련 주석 3곳.
  UUID 해결은 tier-1(per-row own id) → tier-2(어댑터 `discover()`) 2단계로 단순화되었습니다.
- **PyYAML 의존 — 실행 경로 기준으로 해소**: `verify_session.py::mam_orchestrator_uuids` 가
  `yaml` 을 함수 진입 즉시 import 하고 있어(`:10`), tier-3 을 지워도 UUID 해결 경로는 PyYAML 을
  요구했습니다. `state.py` 의 기존 선례대로 YAML 폴백 분기 안으로 이동시켜 교정했습니다.
- **정정**: 원 항목이 서술했던 "`lib.sh` 의 PyYAML 하드 의존" 은 `load_state_json` 이 `state.py` 로
  이관되며 **이미 해소된 상태**였습니다. 한편 `atomic_yaml.py` 는 모듈 존재 이유상 앞으로도
  최상단에서 import 하므로 **저장소 차원의 PyYAML 요구와 설치 게이트는 유지**됩니다.
- 회귀 가드 3종을 신설하고 뮤테이션 5종(M1·M2·M3a·M3b·M4)으로 방어력을 검증했습니다.
```

**주의**: `:5` 의 엣지케이스 카운트와 `:70` §2 헤더는 **반드시 함께** 바꿉니다.

### 7.2 `VERSIONS.md`

`### 🚀 v2.0.0` changelog 에 `#### 7` 추가:

```markdown
#### 7. `agent_identities` tier-3 신원 캐시 완전 제거 및 UUID 해결 경로 PyYAML 탈의존 (B-10 / Option A)
- 쓰기 경로가 존재하지 않아 구조적으로 히트 불가였던 tier-3 폴백과 부속 소비자
  (`workspace_uuid.py`, `reconcile.sh` drift D, `stop_session.sh` 캐시 소거)를 전면 삭제.
- UUID 해결 경로를 **tier-1(per-row own id) → tier-2(어댑터 `discover()`)** 2단계로 단순화.
- `verify_session.py::mam_orchestrator_uuids` 의 즉시 `yaml` import 를 YAML 폴백 분기로 이동,
  UUID 해결 경로가 PyYAML 없이 완주함을 실행 가드로 고정
  (`atomic_yaml.py` 의 시스템 PyYAML 요구는 설계상 유지).
- 회귀 가드 3종 신설 — 읽기 경로 부활 차단, `import yaml` AST 검사(지연 import 포함), 실행 경로 검증.
```

`:44` 의 A-4 인터페이스 나열에서 `identity_cache_fields` 는 §6 Option A 에 따라 **유지**합니다.

---

## 8. 검증 절차

| # | 명령 / 확인 | 기대 |
|---|---|---|
| 1 | `bash -n` — `lib.sh`, `reconcile.sh`, `stop_session.sh` | 3/3 OK (클론 실측 완료) |
| 2 | `python -c "import ast; ast.parse(open('workspace_uuid.py').read())"` | OK (클론 실측 완료) |
| 3 | `grep -rn "agent_identities" .agents/skills/` | 주석 외 **0건** |
| 4 | `grep -rn "tier-3\|3-tier" .agents/skills/` | **0건** |
| 5 | `grep -n sqlite3 lib_py/workspace_uuid.py` | **0건** |
| 6 | `grep -n "yaml" lib_py/verify_session.py` | 폴백 분기 내부 1건만 |
| 7 | 라이브 워크스페이스에서 `find_workspace_uuid <ws> claude` | 변경 전과 **동일 출력** |
| 8 | `reconcile.sh` 1회 실행 후 `drifts` 클래스 집합 | D 미출현, A/B/C 정상 |
| 9 | **뮤테이션 M1·M2·M3a·M3b·M4** | 각각 해당 가드 **FAIL** (§4.2 재현) |
| 10 | `pytest tests/ -q` | **266 passed** (263 실측 + 가드 3건) |
| 11 | `env -u PYTHONPATH pytest tests/test_tier1_unit.py -q` | 전부 통과 (환경 비의존) |
| 12 | `IMPROVEMENTS.md` `:5` ↔ `:70` 대조 | 엣지 카운트 일치 |
| 13 | `IMPROVEMENTS.md` `:6` ↔ `:92` 대조 | 둘 다 22건 |

7번이 **동작 동일성 핵심 검증**입니다 — tier-3 이 히트 불가였다는 주장이 맞다면 출력이 바뀌어서는 안 됩니다.

10번은 약 6분 30초 소요됩니다. 백그라운드 실행 권장.

---

## 9. 규모 및 리스크

| 파일 | 변경 |
|---|---|
| `lib_py/workspace_uuid.py` | −28줄, `sqlite3` import 제거 |
| `lib_py/verify_session.py` | **🆕 yaml import 1줄 이동** |
| `reconcile.sh` | −35줄 (import 는 **보존**) |
| `stop_session.sh` | −6줄 + 주석 1곳 |
| `lib.sh` | 주석 2곳 |
| `base.py` | `identity_cache_fields` 근거 docstring |
| SKILL.md 3종 | drift D 행·예시 1줄·해결 순서 |
| `IMPROVEMENTS.md` / `VERSIONS.md` | 카운트·항목 이동 + changelog |
| `tests/test_tier1_unit.py` | 가드 3건 |
| **테스트 총계** | 263 (실측) → **266** |

| 리스크 | 평가 |
|---|---|
| 동작 회귀 | **낮음.** 제거 대상 전부 생산자 0인 데이터를 읽습니다. 클론 전체 회귀로 확인(§10) |
| 단계 4 부작용 | **낮음.** import 위치만 이동하며 `yaml` 사용 지점은 그대로. `state.py` 에 동일 선례 존재 |
| 레거시 상태 파일 | ⚠️ 구버전 `agent_identities` 가 남은 `.db`/`.yaml` 이 있어도 tier-1·tier-2 가 동일 UUID 를 찾습니다. tier-3 은 앞 두 단계가 모두 실패해야 도달하던 경로이고, **스키마를 지우는 게 아니라 읽기를 멈추는 것**이므로 데이터 파괴 없음 |
| drift D 진단 상실 | **영향 없음.** 생산자 0이므로 한 번도 발화한 적 없음 |
| `identity_cache_fields` 고아화 | §6 Option A + 근거 docstring 으로 차단 |
| 가드 무력화 | §4.2 뮤테이션 5종으로 차단 |

### 권장 커밋 분할

1. `refactor(uuid): drop the dead agent_identities tier-3 fallback (B-10)` — 단계 1~3
2. `fix(verify): defer the yaml import so UUID resolution runs without PyYAML (B-10)` — 단계 4
3. `test(b10): guard the read path, the yaml import, and the executed resolution path` — §4
4. `docs: sync comments, SKILL.md resolution order, IMPROVEMENTS.md and VERSIONS.md for B-10` — 단계 5~6 + §7

3번을 1·2번 뒤에 두면 가드가 앞 커밋 없이 실패하고 함께는 통과함을 커밋 순서로 증명할 수 있습니다. 2번을 분리하는 이유는 이것이 **읽기 경로 제거와 독립된 생산 코드 수정**이기 때문입니다 — 되돌릴 일이 생기면 따로 되돌릴 수 있어야 합니다.

---

## 10. 한계

- 본 계획은 Planner 산출물이며 **저장소 파일을 수정하지 않았습니다**(작업 트리 계획 전후 clean). 모든 실측은 격리 클론(`git clone --local --no-hardlinks`)에서 수행했고 클론은 삭제했습니다.
- 단계 1~4 를 클론에 선적용해 구문 검사·가드·뮤테이션·전체 회귀를 실측했습니다. 단계 5~6(주석·문서)은 실행 대상이 아니므로 적용하지 않았습니다.
- **클론 전체 회귀 실측 완료**: 단계 1~4 + 가드 3종을 적용한 클론에서 **266 passed in 408.18s (0:06:48)** — 기준 베이스라인 `7e21077` 의 263 passed in 408.21s 대비 **정확히 가드 3건 순증, 회귀 0건**입니다. 즉 §8-10 의 266 목표치는 예측이 아니라 실측값입니다. 다만 이는 클론 측정이므로 구현 후 저장소에서 재확인하십시오.
- 제안 가드 3종의 함수 수가 3이므로 266 을 목표치로 잡았습니다. 구현 시 함수를 병합하면 수치를 그에 맞춰 조정하십시오.
- 사용자 환경의 레거시 `.db`/`.yaml` 에 실제로 `agent_identities` 가 남아 있는지는 **이 워크스페이스 한 곳만** 확인했습니다(부재).
- `verify_session.py` 의 `yaml` 지연화는 `mam_orchestrator_uuids` 한 함수만 대상으로 했습니다. 같은 파일의 다른 함수는 감사하지 않았습니다(`grep` 상 `yaml` 참조는 이 함수에만 존재).
