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
This commit is contained in:
2026-08-17 10:59:03 +09:00
parent 7e21077ded
commit 40576c44ab
14 changed files with 639 additions and 114 deletions
@@ -0,0 +1,435 @@
# 📐 구현 계획서 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` 참조는 이 함수에만 존재).
@@ -0,0 +1,105 @@
# Cross-Code Review Report: B-10 — `agent_identities` tier-3 신원 캐시 완전 제거 (Option A)
**Job ID**: `2f64681f`
**Reviewer**: cline
**Date**: 2026-08-17
**Changeset**: 12 files, +99/-114 lines (git diff HEAD)
---
## 1. Changeset Overview
The B-10 backlog item (Option A) eliminates the dead `agent_identities` reading path and tier-3 fallback, removes PyYAML dependency from `workspace_uuid.py`, and simplifies UUID lookup to a 2-tier resolution (tier-1: per-row own id → tier-2: adapter `discover()`).
### Files Changed (12 files)
| File | Change | Lines |
|---|---|---|
| `lib.sh` | Comment updates: 3-tier → 2-tier description | +4/-9 |
| `lib_py/agents/base.py` | Docstring added to `identity_cache_fields` (retention rationale) | +4/-0 |
| `lib_py/verify_session.py` | `import yaml` moved from top-level to YAML fallback branch | +2/-1 |
| `lib_py/workspace_uuid.py` | Tier-3 fallback block removed (32 lines); `sqlite3` import removed | +1/-33 |
| `multi-agent-mux-monitor/SKILL.md` | "### D. Stale UUID" detailed section removed | +0/-10 |
| `multi-agent-mux-monitor/scripts/reconcile.sh` | Drift D detection code removed (37 lines) | +0/-37 |
| `multi-agent-mux-resume/SKILL.md` | UUID resolution order updated to 2-tier | +4/-7 |
| `multi-agent-mux-status/SKILL.md` | Drift class D table row removed | +0/-1 |
| `multi-agent-mux-stop/scripts/stop_session.sh` | Cache clearing code removed (6 lines); comment updated | +1/-7 |
| `IMPROVEMENTS.md` | B-10 moved from open to completed; counts updated | +12/-9 |
| `VERSIONS.md` | B-10 entry added under v2.0.0 item 7 | +7/-0 |
| `tests/test_tier1_unit.py` | 3 new regression tests (B-10 guards) | +64/-0 |
---
## 2. Verification Results
### 2.1 Lint (린트) — ✅ PASS
| Check | Method | Result |
|---|---|---|
| `bash -n lib.sh` | Syntax check | ✅ OK |
| `bash -n reconcile.sh` | Syntax check | ✅ OK |
| `bash -n stop_session.sh` | Syntax check | ✅ OK |
| `py_compile workspace_uuid.py` | Python compile check | ✅ OK |
| `py_compile verify_session.py` | Python compile check | ✅ OK |
| `py_compile base.py` | Python compile check | ✅ OK |
| `agent_identities` in production code | `grep -rn` across 4 target files | ✅ NO MATCHES (even in comments) |
| `import yaml` in workspace_uuid.py | `grep -n yaml` | ✅ NO MATCHES — PyYAML dependency removed |
| `import yaml` in verify_session.py | `grep -n import yaml` | ✅ Only at line 50 (inside YAML fallback branch) |
| IMPROVEMENTS.md header counts | `grep` + `wc -l` | ✅ 3 open (1 arch + 2 edge), 22 completed |
| Section 2 item count | `sed` + `grep -c` | ✅ 2 items (B-13, B-9) — B-10 removed |
| Section 5 item count | `sed` + `grep -c` | ✅ 22 entries (matches header list) |
| B-10 in completed list | `grep B-10` | ✅ In header line 6, section 5 detailed entry, update date |
| VERSIONS.md B-10 entry | `grep -n B-10` | ✅ Line 74, item 7 under v2.0.0 |
| 3 new B-10 tests | `grep -n 'def test_b10'` | ✅ All 3 present (lines 343, 364, 382) |
| `identity_cache_fields` retention | `grep -rn` in adapters | ✅ Retained in base.py + 4 adapters with docstring |
### 2.2 Operability (동작성) — ✅ PASS
| Check | Method | Result |
|---|---|---|
| B-10 regression tests | `pytest -k b10` | ✅ 3/3 PASSED (0.04s) |
| `test_b10_no_agent_identities_reader_in_production` | Unit test | ✅ PASSED — guards against agent_identities read path resurrection |
| `test_b10_workspace_uuid_has_no_yaml_import` | AST analysis | ✅ PASSED — guards against `import yaml` reintroduction (including lazy) |
| `test_b10_find_workspace_uuid_runs_without_pyyaml` | Subprocess stub | ✅ PASSED — UUID resolution path completes without PyYAML |
| Full regression suite | `pytest tests/ -v` | ✅ **266/266 PASS (100%) in 411.09s** |
### 2.3 Loss (유실) — ✅ PASS
| Check | Method | Result |
|---|---|---|
| Tier-3 fallback removed from workspace_uuid.py | `git diff` | ✅ 32-line block removed; `sqlite3` import removed |
| Drift D removed from reconcile.sh | `git diff` | ✅ 37-line block removed (claude/agy/hermes/cline stale UUID checks) |
| Cache clearing removed from stop_session.sh | `git diff` | ✅ 6-line block removed (agent_identities purge on --purge-conversation) |
| PyYAML removed from workspace_uuid.py | `grep yaml` | ✅ No yaml references at all |
| PyYAML moved to lazy import in verify_session.py | `git diff` | ✅ `import yaml` now inside YAML fallback `try` block (line 50) |
| UUID resolution simplified to 2-tier | Code inspection | ✅ tier-1 (per-row own id) → tier-2 (adapter discover()) → print('') |
| `identity_cache_fields` retained intentionally | `grep` + docstring | ✅ Retained with docstring explaining future cache write path need |
| SKILL.md docs updated (resume, monitor, status) | `git diff` | ✅ All 3 docs updated to reflect 2-tier resolution and drift D removal |
| Comments updated (lib.sh, stop_session.sh) | `git diff` | ✅ "3-tier" → "2-tier", tier-3 references removed |
| No production code reads agent_identities | Regression test | ✅ `test_b10_no_agent_identities_reader_in_production` guards this |
---
## 3. Minor Non-Blocking Observations
1. **Monitor SKILL.md line 175**: The "Drift responses" summary list still contains "- D. Stale UUID: report only, no YAML change" even though the detailed "### D. Stale UUID" section and the reconcile.sh drift D implementation were both removed. This summary reference was outside the diff hunk and was not cleaned up. **Non-blocking** — the implementation is correctly removed; only a documentation summary line is stale. Consider removing line 175 in a future cleanup.
2. **`identity_cache_fields` retention**: The `identity_cache_fields` property is retained in `base.py` and all 4 adapters (claude, agy, hermes, cline) with a Korean docstring explaining that while the read path has zero production consumers post-B-10, it remains the sole schema description needed when a cache write path is introduced. This is an intentional, documented design decision — not dead code to remove.
3. **`import yaml` in verify_session.py**: The `yaml` import is now inside a conditional branch (YAML fallback at line 50), only executed when `.db` doesn't contain `orchestrator_uuids` and the YAML file exists. This follows the existing `state.py` precedent for lazy YAML imports. The UUID resolution path can complete without PyYAML when the `.db` file has the data, as verified by `test_b10_find_workspace_uuid_runs_without_pyyaml`.
---
## 4. Verdict
The B-10 backlog item has been correctly resolved via Option A (complete elimination):
- **`agent_identities` read paths removed** from all 3 production locations: `workspace_uuid.py` (tier-3 fallback, 32 lines), `reconcile.sh` (drift D detection, 37 lines), `stop_session.sh` (cache clearing on purge, 6 lines).
- **PyYAML dependency removed** from `workspace_uuid.py` (no `import yaml` at all) and deferred to a lazy conditional import in `verify_session.py` (YAML fallback branch only).
- **UUID resolution simplified** to a clean 2-tier model: tier-1 (per-row own id from `herdr_sessions[]`) → tier-2 (adapter `discover()` on-disk scan) → empty result.
- **Regression guards** (3 new tests) protect against resurrection of the `agent_identities` read path, reintroduction of `import yaml` in `workspace_uuid.py` (including lazy imports via AST analysis), and runtime PyYAML dependency in the resolution path.
- **Documentation updated** consistently across 6 files: `lib.sh` comments, 3 SKILL.md files, `IMPROVEMENTS.md` (B-10 moved to completed, counts updated to 3 open / 22 completed), `VERSIONS.md` (entry 7 under v2.0.0).
- **Full regression suite passes**: 266/266 PASS (100%) in 411.09s.
- The `identity_cache_fields` property is intentionally retained with documentation for future cache write path use.
[VERDICT: PASS]
+4 -9
View File
@@ -1298,15 +1298,10 @@ verify_tui_viewport() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# find_workspace_uuid <workspace> <agent> # find_workspace_uuid <workspace> <agent>
# #
# Workspace-SCOPED resolution of the resume UUID (P0-C). It NEVER returns a # Workspace-SCOPED resolution of the resume UUID (P0-C). Resolution order:
# global agent_identities id unless that id's project_cwd matches THIS
# workspace. Resolution order:
# 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id # 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id
# (claude_session_id_own / agy_conversation_id_own) # (claude_session_id_own / agy_conversation_id_own)
# 2) on-disk scan scoped to this workspace # 2) on-disk scan scoped to this workspace, via the agent adapter's discover()
# (claude: ~/.claude/projects/<key>/*.jsonl ; agy: last_conversations.json[cwd])
# 3) agent_identities cache in d (primary) or $YAML_PATH (fallback), ONLY when its project_cwd == this workspace.
# (Note: DB is authority, YAML is mirror; tier-3 never prioritizes mirror over DB)
# Prints the UUID on stdout (empty line if none). Always exits 0. # Prints the UUID on stdout (empty line if none). Always exits 0.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
find_workspace_uuid() { find_workspace_uuid() {
@@ -1323,8 +1318,8 @@ PYEOF
# #
# Thin wrapper over find_workspace_uuid: resolves THIS workspace's conversation # Thin wrapper over find_workspace_uuid: resolves THIS workspace's conversation
# id (claude jsonl sessionId / agy db uuid) and prints it on stdout (empty line # id (claude jsonl sessionId / agy db uuid) and prints it on stdout (empty line
# if none). find_workspace_uuid is already a workspace-scoped, 3-tier, race-free # if none). find_workspace_uuid is already a workspace-scoped, 2-tier, race-free
# resolver (per-row own id -> workspace-scoped disk scan -> cwd-matched cache), # resolver (per-row own id -> workspace-scoped disk scan),
# so recording its result into the row before kill guarantees tier-1 on the next # so recording its result into the row before kill guarantees tier-1 on the next
# resume. Pass session_name to prefer that specific row's recorded id. Always exits 0. # resume. Pass session_name to prefer that specific row's recorded id. Always exits 0.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+4
View File
@@ -55,6 +55,10 @@ class BaseAgentAdapter:
@property @property
def identity_cache_fields(self) -> tuple: def identity_cache_fields(self) -> tuple:
"""agent_identities 캐시의 에이전트별 필드명.
B-10(Option A)로 캐시 읽기 경로가 제거되어 현재 생산 소비자는 0건이지만,
캐시 쓰기 경로가 도입되면 즉시 필요한 유일한 스키마 기술이므로 존치한다.
임의 삭제 금지 — 삭제 시 4개 어댑터에 필드명을 다시 흩뿌려야 한다."""
raise NotImplementedError raise NotImplementedError
@property @property
+2 -1
View File
@@ -7,7 +7,7 @@ def mam_orchestrator_uuids():
global _MAM_ORC_CACHE global _MAM_ORC_CACHE
if _MAM_ORC_CACHE is not None: if _MAM_ORC_CACHE is not None:
return _MAM_ORC_CACHE return _MAM_ORC_CACHE
import os, sys, json, sqlite3, yaml import os, sys, json, sqlite3
override = os.environ.get("MAM_ORCHESTRATOR_UUIDS") override = os.environ.get("MAM_ORCHESTRATOR_UUIDS")
if override is not None: if override is not None:
if not override.strip(): if not override.strip():
@@ -47,6 +47,7 @@ def mam_orchestrator_uuids():
pass pass
if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p): if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p):
try: try:
import yaml
with open(yaml_p) as f: with open(yaml_p) as f:
d_obj = yaml.safe_load(f) or {} d_obj = yaml.safe_load(f) or {}
except Exception: except Exception:
+1 -33
View File
@@ -1,7 +1,7 @@
# workspace_uuid.py — workspace UUID discovery logic # workspace_uuid.py — workspace UUID discovery logic
# Extracted from lib.sh find_workspace_uuid PYEOF block # Extracted from lib.sh find_workspace_uuid PYEOF block
import os, sys, json, sqlite3 import os, sys, json
from lib_py.verify_session import verify_session_uuid, mam_orchestrator_uuids, mam_row_own_uuid from lib_py.verify_session import verify_session_uuid, mam_orchestrator_uuids, mam_row_own_uuid
OWN_KEY = { OWN_KEY = {
@@ -80,38 +80,6 @@ def find_workspace_uuid_main():
for cand in adapter.discover(ctx): for cand in adapter.discover(ctx):
emit(cand) emit(cand)
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)
conn.execute('PRAGMA busy_timeout = 60000')
try:
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row:
ai = json.loads(row[0]).get('agent_identities') or {}
except sqlite3.OperationalError:
pass
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 = {}
ai_agent = ai.get(agent) or {}
if ai_agent.get('project_cwd') == ws:
cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
print('') print('')
if __name__ == '__main__': if __name__ == '__main__':
@@ -137,16 +137,6 @@ YAML: no such session
``` ```
- **C-ambiguous. Multiple candidates detected**: If multiple candidate transcripts match an unassigned session, the monitor avoids random pinning, reports `C-ambiguous`, and sets `last_visible_status: "ambiguous: N candidates"`. - **C-ambiguous. Multiple candidates detected**: If multiple candidate transcripts match an unassigned session, the monitor avoids random pinning, reports `C-ambiguous`, and sets `last_visible_status: "ambiguous: N candidates"`.
### D. Stale UUID (artifact gone)
```
YAML: agent_identities.claude.session_id=87dc548e-...
disk: ~/.claude/projects/.../87dc548e-...jsonl: missing
→ report it, but DO NOT delete from YAML
(the user may have moved the file or the disk may be temporarily unavailable;
only `--purge-conversation` should remove the id)
```
## Pitfalls ## Pitfalls
- **Don't expect `--once` to stay alive** — it does a single pass and exits. Use `--subscribe` for continuous monitoring. - **Don't expect `--once` to stay alive** — it does a single pass and exits. Use `--subscribe` for continuous monitoring.
@@ -790,43 +790,6 @@ for s in d.get('herdr_sessions', []):
else: else:
_pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=True) _pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=True)
# === drift D: stale UUID (cache 의 artifact 가 사라짐) — 보고만, 변경 없음 ===
ai = d.get('agent_identities', {}) or {}
cl = (ai.get('claude') or {})
if cl.get('session_id'):
sid = cl['session_id']
if not glob.glob(f"{claude_project_dir}/*/{sid}.jsonl"):
drifts.append({'class': 'D', 'name': '(claude identity cache)',
'msg': f"stale UUID in agent_identities.claude.session_id: {sid} (jsonl missing)"})
ag = (ai.get('agy') or {})
if ag.get('conversation_id'):
cid = ag['conversation_id']
if not os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"):
drifts.append({'class': 'D', 'name': '(agy identity cache)',
'msg': f"stale UUID in agent_identities.agy.conversation_id: {cid} (.db missing)"})
hr = (ai.get('hermes') or {})
if hr.get('session_id'):
sid = hr['session_id']
hdb = f"{home}/.hermes/state.db"
has_session = False
if os.path.exists(hdb):
try:
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (sid,)).fetchone()
conn.close()
has_session = r is not None
except Exception:
pass
if not has_session:
drifts.append({'class': 'D', 'name': '(hermes identity cache)',
'msg': f"stale UUID in agent_identities.hermes.session_id: {sid} (session missing from db)"})
cn = (ai.get('cline') or {})
if cn.get('session_id'):
sid = cn['session_id']
if not os.path.exists(f"{home}/.cline/data/sessions/{sid}/{sid}.json"):
drifts.append({'class': 'D', 'name': '(cline identity cache)',
'msg': f"stale UUID in agent_identities.cline.session_id: {sid} (session file missing)"})
result = { result = {
'timestamp': now_iso, 'timestamp': now_iso,
'yaml_path': yaml_path, 'yaml_path': yaml_path,
@@ -47,15 +47,12 @@ ideal resume path:
## UUID resolution order ## UUID resolution order
`agent-sessions.yaml` is the *primary* source. The skill reads in this order: `agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order:
1. **`agent-sessions.yaml``agent_identities.<agent>.session_id` (claude) / `conversation_id` (agy)** — explicit saved value 1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `cline_conversation_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free).
2. **`agent-sessions.yaml``agent_identities.<agent>.session_jsonl` (claude) / `conversation_db` (agy)** — the on-disk artifact 2. **Workspace-scoped on-disk scan** (adapter `discover()`)
3. **Fallback: scan disk for the workspace's most recent conversation** (Note: `CLAUDE_PROJECT_DIR` overrides the default `~/.claude/projects/` path, and `HOME_DIR` overrides the `~` path) —
- claude: `ls -t $CLAUDE_PROJECT_DIR/<workspace-key>/*.jsonl | head -1` and parse the `sessionId` from the first line
- agy: `jq -r '."<workspace>"' $HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json`
If all three are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`. If both are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`.
## Workflow ## Workflow
@@ -105,7 +105,6 @@ lab-paper-pdf2md-creator-claude default running alive clau
| `A` | YAML `running`, herdr dead | session died without going through `multi-agent-mux-stop`. *Could* auto-terminate but won't — that's `multi-agent-mux-monitor`'s job. | | `A` | YAML `running`, herdr dead | session died without going through `multi-agent-mux-stop`. *Could* auto-terminate but won't — that's `multi-agent-mux-monitor`'s job. |
| `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or multi-agent-mux-stop to clean up." | | `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or multi-agent-mux-stop to clean up." |
| `C` | YAML has `claude_session_id_own: null` AND a new *.jsonl exists | new session id materialized; suggest: "run multi-agent-mux-resume or reconcile to register it." | | `C` | YAML has `claude_session_id_own: null` AND a new *.jsonl exists | new session id materialized; suggest: "run multi-agent-mux-resume or reconcile to register it." |
| `D` | YAML has UUID in `agent_identities`, but the on-disk artifact is gone | stale UUID; user should `multi-agent-mux-stop --purge-conversation` to clean up. |
## Pitfalls ## Pitfalls
@@ -164,7 +164,7 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
fi fi
# 캡처: kill 직전에 conversation id 를 해결 (process/jsonl 이 아직 살아있을 때). # 캡처: kill 직전에 conversation id 를 해결 (process/jsonl 이 아직 살아있을 때).
# find_workspace_uuid 가 tier-1(row) -> tier-2(workspace-scoped disk scan) -> tier-3(cache) # find_workspace_uuid 가 tier-1(row) -> tier-2(workspace-scoped disk scan)
# 를 알아서 시도하므로 herdr 생사와 무관하게 동작. # 를 알아서 시도하므로 herdr 생사와 무관하게 동작.
CAPTURED_UUID="" CAPTURED_UUID=""
if [ "$CAPTURE_ID" = "1" ] && [ -n "$TARGET_CWD" ]; then if [ "$CAPTURE_ID" = "1" ] && [ -n "$TARGET_CWD" ]; then
@@ -284,12 +284,6 @@ if purge and purge_uuid:
for item in adapter.purge_artifacts(purge_uuid, ctx): for item in adapter.purge_artifacts(purge_uuid, ctx):
print(f"purged: {item}", flush=True) print(f"purged: {item}", flush=True)
target[adapter.own_key] = None target[adapter.own_key] = None
# agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다
ai = (d.get('agent_identities') or {}).get(agent) or {}
if ai.get('project_cwd') == ws:
if adapter and (ai.get('session_id') == purge_uuid or ai.get('conversation_id') == purge_uuid):
for field in adapter.identity_cache_fields:
ai[field] = None
elif purge and not purge_uuid: elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True) print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)
+12 -9
View File
@@ -1,9 +1,9 @@
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`) # 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
- **최종 갱신일**: 2026-08-17 (B-5 macOS NFS 감지 df -P 폴백 검증 및 종결 완료, C-6 완료, 전체 263/263 회귀 통과 반영) - **최종 갱신일**: 2026-08-17 (B-10/P3-2 agent_identities tier-3 신원 캐시 제거 및 PyYAML 탈의존 완료, B-5 종결, C-6 완료, 전체 266/266 회귀 통과 반영)
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` - **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md`
- **총 추적 미해결 과제**: **4** (아키텍처 1건, 엣지케이스 3건, 오케스트레이션 0건, 레거시 잔재 0건) - **총 추적 미해결 과제**: **3** (아키텍처 1건, 엣지케이스 2건, 오케스트레이션 0건, 레거시 잔재 0건)
- **완료된 과제**: **21** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup) - **완료된 과제**: **22** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-10, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup)
--- ---
@@ -67,7 +67,7 @@
--- ---
## 2. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 3건) ## 2. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 2건)
### **B-13: 셀프 호스팅 멀티에이전트 루프 중 턴 간 스킬 오염 (In-Flight Tooling Mutation)** — 🟡 **Stage 2 분리 과제** ### **B-13: 셀프 호스팅 멀티에이전트 루프 중 턴 간 스킬 오염 (In-Flight Tooling Mutation)** — 🟡 **Stage 2 분리 과제**
- `/multi-agent-mux-loop``multi-agent-mux` 자체를 수정/리팩터링할 때, Turn 1에서 Worker가 `.agents/skills/...` 를 수정(문법 오류나 미완성 코드 포함)하면 후속 Turn 2의 Reviewer 잡 제출 시 래퍼가 비정상 종료되어 자가 치유(Corrective/Rebuttal) 단계로 진입하지 못하고 루프가 중단될 수 있는 위험입니다. - `/multi-agent-mux-loop``multi-agent-mux` 자체를 수정/리팩터링할 때, Turn 1에서 Worker가 `.agents/skills/...` 를 수정(문법 오류나 미완성 코드 포함)하면 후속 Turn 2의 Reviewer 잡 제출 시 래퍼가 비정상 종료되어 자가 치유(Corrective/Rebuttal) 단계로 진입하지 못하고 루프가 중단될 수 있는 위험입니다.
@@ -76,9 +76,6 @@
### **B-9: `LOGS_DIR` import 시점 cwd 고정** ### **B-9: `LOGS_DIR` import 시점 cwd 고정**
- `mqtt_common.py` 모듈 로드 시점의 cwd로 감사 로그 경로가 1회 고정됩니다. - `mqtt_common.py` 모듈 로드 시점의 cwd로 감사 로그 경로가 1회 고정됩니다.
### **B-10: `agent_identities` 쓰기 경로 부재 및 PyYAML 의존성**
- 저장소 전체에 `agent_identities` 를 생성·갱신하는 코드가 0건이며, `lib.sh` 의 PyYAML 하드 의존성으로 인해 `.db` 만으로 충분한 경우에도 PyYAML 부재 시 상태 조회가 무력화되는 문제가 존재합니다.
--- ---
## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 0건 — 전원 완료) ## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 0건 — 전원 완료)
@@ -89,7 +86,13 @@
--- ---
## 5. 🎉 완료된 과제 (Completed Tasks — 21건) ## 5. 🎉 완료된 과제 (Completed Tasks — 22건)
### **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종(`test_b10_no_agent_identities_reader_in_production`, `test_b10_workspace_uuid_has_no_yaml_import`, `test_b10_find_workspace_uuid_runs_without_pyyaml`)을 신설하고 뮤테이션 5종(M1·M2·M3a·M3b·M4)으로 방어력을 검증했습니다.
### **B-5: macOS NFS 감지 `df -P` 폴백 검증 및 종결** — ✅ 완료 (종결) ### **B-5: macOS NFS 감지 `df -P` 폴백 검증 및 종결** — ✅ 완료 (종결)
- `_check_is_nfs`(`lib.sh:1181-1192`)에서 macOS/BSD 환경 시 GNU 전용 `df --output=target` 실패(`rc=64`)에 대비한 `df -P "$f" | tail -1 | awk '{print $6}'` POSIX 폴백이 정상 동작함을 실측 및 단위 테스트(`test_stop_check_is_nfs_local`)로 검증 완료하여 종결 처리했습니다. - `_check_is_nfs`(`lib.sh:1181-1192`)에서 macOS/BSD 환경 시 GNU 전용 `df --output=target` 실패(`rc=64`)에 대비한 `df -P "$f" | tail -1 | awk '{print $6}'` POSIX 폴백이 정상 동작함을 실측 및 단위 테스트(`test_stop_check_is_nfs_local`)로 검증 완료하여 종결 처리했습니다.
@@ -238,7 +241,7 @@
| **P2-2** | **C-3a + C-4** | 빈 스텁 4종 + 공허한 테스트 4건 + 죽은 심볼 3종 제거 및 `--isolate` no-op 회귀 가드 신설 **(✅ 완료 — tests/test_tier1_unit.py + test_tier2_component.py 256/256 PASS)** | 소 | — | | **P2-2** | **C-3a + C-4** | 빈 스텁 4종 + 공허한 테스트 4건 + 죽은 심볼 3종 제거 및 `--isolate` no-op 회귀 가드 신설 **(✅ 완료 — tests/test_tier1_unit.py + test_tier2_component.py 256/256 PASS)** | 소 | — |
| **P2-3** | **C-6** | `stop_session.sh` 헤더/도움말/주석/MESSAGING.md 정리 및 회귀 가드 신설 **(✅ 완료 — tests/test_tier2_component.py 가드 신설, 전체 263/263 PASS)** | 극소 | — | | **P2-3** | **C-6** | `stop_session.sh` 헤더/도움말/주석/MESSAGING.md 정리 및 회귀 가드 신설 **(✅ 완료 — tests/test_tier2_component.py 가드 신설, 전체 263/263 PASS)** | 극소 | — |
| **P3-1** | **A-4 M2~M7** | 어댑터 본이관 및 CLI facts 브리지/서브커맨드 구축 **(✅ 완료 — tests/test_a4_adapter_contract.py 9/9 PASS, 전체 259/259 PASS)** | 대 | P1-1 | | **P3-1** | **A-4 M2~M7** | 어댑터 본이관 및 CLI facts 브리지/서브커맨드 구축 **(✅ 완료 — tests/test_a4_adapter_contract.py 9/9 PASS, 전체 259/259 PASS)** | 대 | P1-1 |
| **P3-2** | **B-10** | tier-3 신원 캐시 존치/제거 결정 + PyYAML 의존 완화 | 중 | A-4 M2 | | **P3-2** | **B-10** | tier-3 신원 캐시 완전 제거 및 UUID 경로 PyYAML 의존 (Option A) **(✅ 완료 — tests/test_tier1_unit.py 가드 3건 신설, 전체 266/266 PASS)** | 중 | A-4 M2 |
| **P3-3** | **C-3b** | `isolation.root` 4개 소비자 완전 폐기 (Option B 채택) **(✅ 완료 — 전체 259/259 PASS)** | 소 | A-4 M2 | | **P3-3** | **C-3b** | `isolation.root` 4개 소비자 완전 폐기 (Option B 채택) **(✅ 완료 — 전체 259/259 PASS)** | 소 | A-4 M2 |
| **P4-1** | **B-9** | 기본값 한정. `logs_dir` 인자·`DELEGATE_JOB_LOGS_DIR` 두 가지 회피 수단 존재 | 극소 | — | | **P4-1** | **B-9** | 기본값 한정. `logs_dir` 인자·`DELEGATE_JOB_LOGS_DIR` 두 가지 회피 수단 존재 | 극소 | — |
| **P5-1** | **A-2** | 공개 브로커 및 HMAC 검증 보완 (📌 *사용자 지침: 차후 전용 MQTT 브로커 서빙 환경 구축 시점에 진행*) | 중 (3파일) | 전용 브로커 | | **P5-1** | **A-2** | 공개 브로커 및 HMAC 검증 보완 (📌 *사용자 지침: 차후 전용 MQTT 브로커 서빙 환경 구축 시점에 진행*) | 중 (3파일) | 전용 브로커 |
+7
View File
@@ -71,6 +71,13 @@
#### 6. macOS NFS 감지 `df -P` 폴백 검증 및 종결 (B-5) #### 6. macOS NFS 감지 `df -P` 폴백 검증 및 종결 (B-5)
- `_check_is_nfs`(`lib.sh`)의 macOS/BSD 환경 내 GNU 전용 `df --output` 구문 오류 시 POSIX `df -P` 폴백 동작을 실측 및 단위 테스트(`test_stop_check_is_nfs_local`)로 검증 완료하여 B-5 이슈를 정식 종결. - `_check_is_nfs`(`lib.sh`)의 macOS/BSD 환경 내 GNU 전용 `df --output` 구문 오류 시 POSIX `df -P` 폴백 동작을 실측 및 단위 테스트(`test_stop_check_is_nfs_local`)로 검증 완료하여 B-5 이슈를 정식 종결.
#### 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 포함), 실행 경로 검증.
- 회귀 및 계약 테스트: **266/266 PASS (100%)** 달성.
--- ---
### 🛠️ `v1.4.0` — Stability, Cleanup & Safe Job Delegation (2026-08-16) ### 🛠️ `v1.4.0` — Stability, Cleanup & Safe Job Delegation (2026-08-16)
+64
View File
@@ -338,3 +338,67 @@ def test_mqtt_with_retry_failure(mam_sandbox):
with pytest.raises(ValueError, match="failing"): with pytest.raises(ValueError, match="failing"):
failing_func() failing_func()
assert len(calls) == 3 assert len(calls) == 3
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}"