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:
@@ -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]
|
||||||
@@ -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.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
@@ -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파일) | 전용 브로커 |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user