Files
Godopu 40576c44ab refactor(uuid): resolve B-10 by deprecating agent_identities and removing PyYAML dependency
- Remove dead agent_identities read path and PyYAML import from workspace_uuid.py
- Defer eager PyYAML import in verify_session.py to lazy YAML fallback branch
- Simplify UUID resolution to 2-tier model (tier-1 own row ID -> tier-2 adapter scan)
- Clean up unused Drift D and ghost cache clearing in reconcile.sh and stop_session.sh
- Add 3 regression guards in tests/test_tier1_unit.py (266/266 PASS)
- Update IMPROVEMENTS.md, VERSIONS.md, and SKILL.md files
2026-08-17 10:59:03 +09:00

436 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📐 구현 계획서 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` 참조는 이 함수에만 존재).