28 KiB
B-1 구현 계획서 Rev.2 — find_workspace_uuid tier-3 신원 캐시 복구
- Job:
662f07ba(Planner) — 원안64990742Rev.1 을 Creator 이의제기2b9e9b67(agy) 반영하여 개정 - 대상:
IMPROVEMENTS.md§2 / B-1 - 기준 커밋:
b6c41e6(작업 트리 clean) - 작성 원칙: 이의제기의 전제와 처방을 각각 따로 실행하여 판정한다. 상대 코드를 그대로 돌려 보지 않고 논평하지 않는다.
0. 이의제기 판정 요약 (Adjudication)
| 대상 | 판정 | 근거 (실측) |
|---|---|---|
전제 — "d가 truthy인데 agent_identities가 없고, YAML에는 남아 있는 상태에서 폴백이 영구 차단된다" |
기각 (Refuted) | load_state_json과 폴백은 같은 base 경로 · 같은 DB-우선 우선순위로 읽는다. 6가지 디스크 상태 전수 측정 결과 네 변형이 전 셀 동일 |
처방 — if isinstance(d, dict) and d.get('agent_identities'): 로 바꾸면 하위호환 신원이 "완벽히 복원"된다 |
기각 (Refuted) | 제안 코드를 원문 그대로 실행했다. 문제로 지목된 S2·S6 셀에서 여전히 miss. 폴백에는 진입하지만(probe=1) 폴백 역시 DB를 먼저 읽어 YAML에 도달하지 못한다 |
처방의 경로 폴백 — os.environ.get('YAML_PATH','') → 없으면 <ws>/.mam/... 추측 |
거부 (Rejected) | 본 계획서가 제거하려는 D1(경로 추측)을 그대로 재도입한다. 직전 잡 4dbf4feb의 ${1:-$PWD}와 동일한 패턴 |
| 조건 변경 자체 | 채택 (Adopted — 단, 근거는 다름) | 하위호환 복구 목적으로는 무의미하나, load_state_json의 반환 형태 변경에 대한 내성으로는 유효. 재조회 비용 실측 0.063 ms → 성능을 반대 논거로 쓰지 않는다 |
| 원안의 나머지 (D1·D3·D4·D5, V-1…V-6, 커밋 분할) | 유지 | 이의 없음 |
요약: agy가 지적한 셀(S2/S6)은 agy의 코드로도 고쳐지지 않는다. 그 셀을 실제로 고치려면 DB(권위)를 제치고 YAML(미러)을 읽어야 하는데, 이는
atomic_dump_yaml의 설계와 정면으로 충돌한다. 따라서 Rev.2는 조건 변경은 받되(근거 교체), 경로 추측은 거부하고, S2/S6를 "의도된 동작"으로 테스트에 못박는다(V-8).
1. 전제 검증 — d와 폴백은 같은 것을 읽는다
load_state_json(lib.sh:510-545)의 소스 결정 로직:
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
if os.path.exists(db_path): d = json.loads(<state row>) ; d['herdr_sessions'] = <sessions table>
elif os.path.exists(yaml_path): d = yaml.safe_load(f)
Rev.1 폴백과 완전히 동일한 base 경로, 동일한 DB-우선 우선순위다. 따라서
d가 신원을 못 가졌다 ⟹ 폴백도 같은 자리에서 같은 것을 읽어 못 가진다.
이의제기가 상정한 "d에는 없고 디스크에는 있는" 상태가 성립하려면 두 읽기의 소스가 갈려야 하는데
갈릴 수 없다.
1-1. 6가지 디스크 상태 전수 측정
tier-1(관계없는 세션 1행)·tier-2(last_conversations.json 부재)를 굶겨 tier-3만 남긴 상태에서
네 변형을 각각 실행:
| 상태 | 디스크 배치 | HEAD | Rev.1 if d: |
agy 제안 | Rev.2 |
|---|---|---|---|---|---|
| S1 | DB=신원 있음, YAML=신원 있음 | HIT | HIT | HIT | HIT |
| S2 | DB=신원 없음, YAML=신원 있음 ← 이의제기 시나리오 | miss | miss | miss | miss |
| S3 | YAML만 존재, 신원 있음 | HIT | HIT | HIT | HIT |
| S4 | YAML만 존재, 신원 없음 | miss | miss | miss | miss |
| S5 | DB만 존재, 신원 없음 | miss | miss | miss | miss |
| S6 | DB에 state 행 없음 + sessions 행 존재, YAML=신원 있음 | miss | miss | miss | miss |
네 변형이 6/6 셀에서 동일하다. 조건 변경은 해석 결과를 단 한 셀도 바꾸지 못한다.
S3가 HIT인 점에 주목할 것 — "YAML에만 신원이 있는" 정상 상태는 load_state_json이 elif 분기로
YAML 전체를 싣기 때문에 d가 이미 신원을 갖는다. 이의제기가 걱정한 하위호환 경로는
Rev.1에서 이미 살아 있었다.
2. 처방 검증 — 제안 코드를 그대로 실행했다
이의제기 §3의 코드를 한 글자도 바꾸지 않고(폴백 진입 여부를 세는 PROBE 한 줄만 추가) 위 매트릭스에
투입했다. S2 결과:
S2 agy=miss probes=1
probes=1은 폴백에 진입했다는 뜻이다. 그런데도 miss인 이유는 폴백의 첫 분기가
if os.path.exists(db_path): 이기 때문이다. DB가 존재하므로 YAML elif는 평가조차 되지 않고,
DB의 state 행에는 신원이 없으므로 ai = {}로 끝난다. 폴백이 열렸을 뿐 도달하지 못한다.
S6도 같다 — DB는 존재하고 state 행만 없어 row = None → ai = {}.
이의제기 §2-2의 결론 "tier-3 캐시 조회를 100% 영구 차단" 은 Rev.1이 아니라 두 변형 모두에 해당하며, 제안된 수정은 그 차단을 해제하지 못한다. 전제와 처방이 함께 성립하지 않는다.
2-1. S2/S6를 진짜로 고치려면 무엇이 필요한가 (그리고 왜 하지 않는가)
DB에 없고 YAML에만 있는 신원을 살리려면 DB 조회가 비었을 때 YAML로 넘어가는 union 방식이어야 한다
(if ... elif ...가 아니라 if not ai and os.path.exists(yaml_path): ...).
그렇게 하지 않는다. atomic_dump_yaml(lib.sh:682-920)은 SQLite BEGIN IMMEDIATE 트랜잭션을
권위로 삼고 YAML은 그 뒤에 temp+rename으로 쓰는 미러다. DB에서 사라진 신원을 YAML에서 되살리면:
stop_session.sh --purge-conversation이 DB에서 지운 신원이 YAML 미러를 통해 부활한다. (stop_session.sh:333-348이 지우는 대상이 바로 이 캐시다.)- 두 파일이 갈린 상태 = 이미 손상된 상태이며, 손상된 미러를 권위보다 우선시키는 복구는 P0-C 워크스페이스 격리 보증(다른 워크스페이스 UUID 절대 반환 금지) 을 무너뜨릴 수 있다.
따라서 S2/S6의 miss는 버그가 아니라 의도된 동작이며, Rev.2는 이를 V-8로 못박아 후대의 "친절한 복구" 시도를 차단한다.
3. 처방의 경로 폴백 거부
yaml_path = os.environ.get('YAML_PATH', '')
if not yaml_path:
yaml_path = os.path.join(ws, ".mam", "agent-sessions.yaml") # ← D1 재도입
YAML_PATH는 env_python(lib.sh:661-662)이 무조건 설정한다. 이 블록은 env_python을 통해서만
실행되므로 이 가드는 도달 불가능한 죽은 코드다. 그리고 만에 하나 도달한다면(= 이 블록이
env_python 밖에서 실행되는 미래) 그때야말로 워크스페이스 기준 추측이 가장 위험하다 —
AGENT_SESSIONS_YAML이 재정의된 배치에서 조용히 엉뚱한 파일을 읽는다. 이는 본 계획서가
제거하려는 D1 그 자체이며, ea36e81의 if 'mam_dir' in locals()와 구조가 동일한 죽은 가드다.
대신 채택하는 하드닝: 경로 해석을 try 안쪽으로 옮긴다. os.environ['YAML_PATH']를 그대로
쓰되(추측 없음), 만약 미설정이면 KeyError가 except에 잡혀 WARN 한 줄과 ai={}로 수렴하여
rc=0 계약이 유지된다. Rev.1은 이 줄이 try 밖에 있어 KeyError 시 rc=1이 될 수 있었다 —
이의제기가 간접적으로 드러낸 실질적 개선점이며, Rev.2에 반영했다.
(이 경로는 공개 인터페이스로는 재현 불가하다. env_python이 항상 변수를 넣기 때문이다.
따라서 테스트를 붙이지 않고 구조적 방어로만 남긴다 — 검증 불가한 것을 검증했다고 적지 않는다.)
4. 조건 변경 채택 — 근거를 교체하여
해석 결과가 6/6 동일하므로 하위호환 복구를 근거로 한 채택은 불가하다. 그럼에도 조건 자체는 채택한다. 근거는 하나뿐이다:
if d:는 "load_state_json이 병합 상태에agent_identities를 실어 준다"는 암묵적 결합에 의존한다. 이 저장소는 이미 state 블롭에서sessions테이블을 분리한 전례가 있고(현재 라이브 DB의state.data최상위 키는['snapshot']뿐이다), 같은 방식으로 신원이 분리되면if d:는 조용히 폴백을 건너뛴다.
비용은 실측했다: 재조회 1회 = sqlite3.connect + SELECT = 0.063 ms/call (200회 평균, 로컬 SSD).
tier-3은 tier-1·tier-2가 모두 실패했을 때만 도달하는 경로이므로 성능은 반대 논거가 되지 못한다.
성능을 이유로 거부하지 않는다는 점을 명시한다.
다만 조건 변경만으로는 위 결합을 감지하지 못하고 은폐할 뿐이므로(폴백이 조용히 대신 일한다), V-7 결합 테스트를 함께 넣는다. 런타임 우회 + CI 감지 두 겹이 되어야 실제 방어가 된다.
N-1과의 상호작용(중요): 저장소 어디에도 agent_identities 쓰기 코드가 없으므로
(§5 참조) 실사용에서 d.get('agent_identities')는 사실상 항상 falsy다. 즉 조건 변경 후
폴백이 사실상 상시 경로가 된다. 이는 무해하지만, "1차 경로는 재조회 없음"이라던 Rev.1의
설계 근거 ②는 더 이상 성립하지 않는다. D2의 의의는 "재조회 제거"가 아니라
"경로 추측 제거" 로 축소 기술한다.
5. HEAD 실측 (Rev.1에서 유지)
5-1. 현재 코드 (.agents/skills/lib.sh:1266-1282)
ai = {}
db_path = f"{mam_dir}/agent-sessions.db" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.db")
yaml_path = f"{mam_dir}/agent-sessions.yaml" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.yaml")
try:
import yaml
if os.path.exists(db_path):
...
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {} # ← 상태 딕셔너리 d 를 덮어씀
ai = d.get('agent_identities', {})
except Exception as e:
print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
git blame: 1267·1268·1270·1281·1282 행이 ea36e81(2026-08-05), 나머지는 2026-06-21 원본.
B-1 원문의 NameError는 ea36e81에서 이미 제거되었다 — 전제는 낡았고, 증상 대신 경로 추측이 남았다.
5-2. 잔존 결함 재현 매트릭스
| # | 조건 | HEAD | Rev.2 |
|---|---|---|---|
| P1 | 상태 파일이 <ws>/.mam/에 있음 (기본 배치) |
<uuid> ✅ |
<uuid> ✅ |
| P2 | AGENT_SESSIONS_YAML이 <ws>/.mam/ 밖 |
"" 무음 ❌ |
<uuid> ✅ |
| P3 | .db 존재 + PyYAML 없음 |
"" ❌ |
<uuid> ✅ |
| P4 | agent_identities 없음 |
"" ✅ |
"" ✅ |
| P5 | project_cwd 불일치 |
"" ✅ |
"" ✅ |
| P7 | agent_identities가 dict 아님 |
Traceback, rc=1 ❌ |
"", rc=0 ✅ |
| P8 | hermes 신원이 conversation_id 키 |
"" ❌ |
<uuid> ✅ |
6. 결함 목록
D1 — 상태 파일 경로를 추측한다 (핵심)
env_python이 YAML_PATH로 권위 경로를 넘겨주고(lib.sh:661-662), 같은 파일의 다른 두 블록은
이를 지킨다(load_state_json L512, atomic_dump_yaml L746). tier-3만 os.path.join(ws, ".mam", ...)를
직접 조립한다. AGENT_SESSIONS_YAML은 lib.sh:26에서 재정의 가능하고 해석 대상 워크스페이스는
WORKSPACE_ROOT와 같을 의무가 없다 → 갈리는 순간 무음 실패(P2).
if 'mam_dir' in locals() 가드는 mam_dir 정의가 0건이므로 항상 False인 죽은 코드다.
D2 — 이미 로드한 상태를 다시 읽는다 (Rev.2에서 축소 기술)
find_workspace_uuid는 진입 시 MAM_STATE_JSON으로 병합 상태를 받아 d에 담는다(L1130-1133).
tier-3의 두 번째 읽기는 이 중복이 D1·D3·D4를 낳은 구조적 원인이다. 단, §4에 따라 Rev.2는
재조회 자체를 제거하지 않는다 — 경로 추측만 제거한다.
D3 — import yaml이 SQLite 분기까지 죽인다
try 첫 줄의 import yaml 때문에 PyYAML 부재 시 YAML이 필요 없는 .db 분기까지 무력화된다(P3).
도달 가능성: deploy/install.sh:86이 시스템 python3의 PyYAML을 하드 게이트하지만
_delegate_py_bin(L1416-1421)은 $VIRTUAL_ENV를 최우선 선택한다. 무관한 venv 활성 상태에서
게이트를 우회한다 — stub 없이 실제 bare venv로 실측:
picked python: .../barevenv/bin/python
HEAD out=<Traceback ... ModuleNotFoundError: No module named 'yaml'|WARN: tier-3 identity lookup failed: ...|>
D4 — d 섀도잉 + rc=0 계약 위반
YAML 분기의 d = yaml.safe_load(f)가 tier-1/2의 상태 딕셔너리를 덮어쓴다(현재는 이후 미사용이라
무해하나, 한 줄만 추가돼도 즉시 오동작하는 함정). 더 심각한 것은 ai.get(agent)(L1284)가 try
밖이라 비-매핑 입력 시 AttributeError → rc=1이 된다는 점이다(P7). 함수 주석의
"Always exits 0"(L1106) 위반이며, resolve_session_id.sh:44는 set -euo pipefail 아래
마지막 명령이라 그대로 스크립트 종료 코드가 된다(stop_session.sh:140,159는 || true로 방어됨).
D5 — hermes/cline 폴백이 잘못된 딕셔너리를 본다
cand = ai_agent.get('session_id') or ai.get('conversation_id') # L1295, L1299
ai는 {agent: {...}} 맵이므로 ai.get('conversation_id')는 항상 None. cline은 tier-2 디스크
스캔에 가려지지만, hermes는 tier-2가 WHERE cwd=?로 조회하므로 hermes DB의 cwd가 다르면
tier-2가 실패하고 tier-3만 남는다 → 폴백이 죽는다(P8).
7. 인접 발견 (B-1 범위 밖 — 별도 등재)
N-1 — agent_identities 쓰기 코드가 저장소에 0건
전수 조사: lib.sh:1275,1280(읽기), stop_session.sh:334(기존 항목 비우기),
reconcile.sh:690(진단 읽기). 생성·갱신 0건, git log -S"d['agent_identities']"도 공집합.
라이브 워크스페이스의 state.data 최상위 키는 ['snapshot']뿐이고 agent_identities는 null이다.
→ B-1을 완벽히 고쳐도 현행 코드로 생성된 워크스페이스의 복원 건수는 0이다. tier-3은
(a) 마이그레이션된 YAML, (b) 수동 편집 상태를 위한 하위호환 읽기 경로로만 의미를 갖는다.
§4에서 본 것처럼 이 사실은 조건 변경의 실효(폴백이 상시 경로가 됨)에도 직결된다.
N-2 — load_state_json의 PyYAML 하드 의존
lib.sh:511이 모듈 최상단에서 yaml을 import 하여, .db만으로 충분한 경우에도 PyYAML 부재 시
traceback과 함께 d={}가 되어 tier-1·tier-2가 함께 붕괴한다(P3 stderr에서 관측).
8. 왜 기존 55개 테스트가 이를 잡지 못했는가
tests/conftest.py::mam_sandbox는 AGENT_SESSIONS_YAML = tmp_path/.mam/agent-sessions.yaml,
WORKSPACE_ROOT = tmp_path로 두고, 테스트는 find_workspace_uuid {tmp_path} ...로 호출한다.
따라서 추측 경로 <ws>/.mam/agent-sessions.yaml과 권위 경로가 모든 테스트에서 문자 단위로 동일하다.
하네스가 구조적으로 D1을 볼 수 없다.
→ 신규 테스트는 **워크스페이스 디렉터리와 상태 파일 디렉터리를 분리한 split_sandbox**를 쓴다.
9. 구현 계획
원자적·이등분 가능 커밋 4개. F1↔F2↔F3 사이 순서 제약 없음(각 테스트가 서로 독립적으로 red).
F1 — tier-3 상태 소스 교정 (D1·D3·D4 해소, D2 축소) — Rev.2 개정
lib.sh:1266-1282 전체를 아래로 치환한다. (프로토타입 검증 완료)
ai = d.get('agent_identities') if isinstance(d, dict) else None
if not isinstance(ai, dict) or not ai:
ai = {}
try:
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
if row:
ai = json.loads(row[0]).get('agent_identities') or {}
conn.close()
elif os.path.exists(yaml_path):
import yaml
with open(yaml_path) as f:
_ydoc = yaml.safe_load(f) or {}
ai = _ydoc.get('agent_identities') or {}
except Exception as e:
print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
if not isinstance(ai, dict):
ai = {}
Rev.1 → Rev.2 변경 2건:
| # | 변경 | 이유 |
|---|---|---|
| ① | if d: → d에서 신원을 실제로 확보했는지로 분기 |
이의제기 채택. 단 근거는 하위호환이 아니라 load_state_json 반환 형태 변경 내성(§4). V-7과 한 쌍으로만 유효 |
| ② | yaml_path/db_path 해석을 try 안쪽으로 이동 |
YAML_PATH 미설정 시 KeyError가 rc=1이 되는 잔여 계약 위반 차단(§3) |
거부한 변경 1건: os.environ.get('YAML_PATH','') → <ws>/.mam/... 추측 폴백. D1 재도입(§3).
설계 근거:
- 1차 소스는 이미 로드된
d— 정상 경로에서 재조회 없음. - 폴백은
YAML_PATH기반, 추측 없음 — L512/L746과 동일 규약..db파생도 동일. import yaml은 YAML 분기 안 — SQLite 분기가 PyYAML에 인질 잡히지 않는다._ydoc+ 이중isinstance가드 — 섀도잉 제거,rc=0계약 복원.
F2 — hermes/cline 폴백 키 수정 (D5)
- cand = ai_agent.get('session_id') or ai.get('conversation_id')
+ cand = ai_agent.get('session_id') or ai_agent.get('conversation_id')
lib.sh:1295, lib.sh:1299 두 곳. replace_all 금지 — 두 줄이 완전히 동일하므로
문맥(elif agent == 'hermes': / 'cline')을 포함해 개별 치환할 것.
F3 — 회귀 테스트 신설
신규 파일 tests/test_b1_tier3_identity.py (V-1…V-8). 전용 split_sandbox 픽스처를 파일 내부에
둔다 — conftest.py::mam_sandbox를 고치면 기존 55개 테스트의 전제가 흔들리므로 공용 픽스처는
건드리지 않는다.
F4 — 주석·문서 정합
lib.sh:1105tier-3 설명에 상태 소스가d(1차)와$YAML_PATH(폴백)임을 명시.- DB가 권위, YAML은 미러이며 tier-3은 미러를 권위보다 앞세우지 않는다는 한 줄을 추가(§2-1).
IMPROVEMENTS.mdB-1 항목 교체 + B-10/B-11 신규 등재(§11).
10. 테스트 계획 (red-before-green 실측 완료)
| ID | 테스트명 | 검증 대상 | HEAD | Rev.2 |
|---|---|---|---|---|
| V-1 | test_b1_tier3_honours_agent_sessions_yaml_path |
D1 — 워크스페이스≠상태파일 디렉터리 | FAIL | PASS |
| V-2 | test_b1_tier3_db_branch_survives_missing_pyyaml |
D3 — PyYAML 부재 시 SQLite 분기 생존 | FAIL | PASS |
| V-3 | test_b1_tier3_corrupt_identities_still_exits_zero |
D4 — rc=0 계약 |
FAIL | PASS |
| V-4 | test_b1_tier3_hermes_conversation_id_fallback |
D5 — hermes 폴백 키 | FAIL | PASS |
| V-5 | test_b1_tier3_refuses_foreign_workspace_identity |
P0-C 격리 유지 (가드) | PASS | PASS |
| V-6 | test_b1_tier3_absent_identities_is_silent |
무신원 시 무음·무예외 (가드) | PASS | PASS |
| V-7 | test_b1_load_state_json_surfaces_agent_identities |
결합 고정 — load_state_json이 신원을 d에 실어야 함 |
PASS | PASS |
| V-8 | test_b1_tier3_does_not_read_yaml_mirror_behind_the_db |
동작 고정 — stale DB를 YAML 미러로 되살리지 않음 | PASS | PASS |
V-7·V-8은 이의제기 대응으로 신설했다. 둘 다 HEAD에서도 green인 고정(pinning) 테스트다 — red를 만드는 것이 목적이 아니라, §1·§2에서 측정으로 확인한 사실을 코드로 못박아 다음 리팩터에서 조용히 깨지는 것을 막는 것이 목적이다. V-7이 깨지면 §4의 결합이 끊어진 것이고, V-8이 깨지면 누군가 미러를 권위보다 앞세운 것이다.
4개 변형 전수 실행 결과:
HEAD 4 failed, 4 passed in 1.23s
REV1 8 passed in 1.18s (if d:)
AGY 8 passed in 1.19s (이의제기 제안 원문)
REV2 8 passed in 1.20s (채택안)
세 수정안이 동일하게 8 passed라는 사실 자체가 §1의 결론을 다시 확인해 준다 — 조건 변경은 관측 가능한 동작을 바꾸지 않는다.
핵심 픽스처(구현자는 이 구조를 그대로 쓸 것):
@pytest.fixture
def split_sandbox(tmp_path):
"""워크스페이스와 상태 파일을 서로 다른 디렉터리에 둔다 — mam_sandbox 의 우연을 깬다."""
shutil.copytree(os.path.join(REPO, ".agents", "skills"), tmp_path / ".agents" / "skills")
ws = tmp_path / "ws"; (ws / ".mam").mkdir(parents=True)
state_dir = tmp_path / "state"; state_dir.mkdir()
home = tmp_path / "home"; (home / ".claude" / "projects").mkdir(parents=True)
return {"root": tmp_path, "ws": ws, "yaml": state_dir / "agent-sessions.yaml",
"home": home, "lib": tmp_path / ".agents" / "skills" / "lib.sh"}
V-2의 PyYAML 부재는 스텁 모듈(PYTHONPATH=<tmp>/noyaml의 yaml/__init__.py에서 raise ImportError)로
결정적으로 재현한다 — 실제 venv 생성 없이 CI에서 빠르고 안정적이다.
검증에 사용한 전체 테스트 파일(8케이스)은 저장소 밖 스크래치패드에 그대로 있다:
/private/tmp/claude-501/.../scratchpad/b1/test_b1_tier3_identity.py
11. 검증 게이트
| 게이트 | 명령 | 통과 기준 | 실측 |
|---|---|---|---|
| G-A | bash -n .agents/skills/lib.sh |
구문 통과 | 통과 |
| G-B | pytest tests/test_b1_tier3_identity.py -q (수정 전) |
4 failed, 4 passed | 확인 |
| G-C | pytest tests/test_b1_tier3_identity.py -q (수정 후) |
8 passed | 확인 |
| G-D | pytest tests/test_tier1_unit.py tests/test_tier2_component.py -q |
55 passed (회귀 0) | Rev.2 패치 클론에서 55 passed / 459.81s |
| G-E | pytest tests/test_tier2_component.py -k "find_workspace_uuid or resume or stop" |
10 passed | 확인 (13.3s) |
- G-D는 MQTT 왕복 때문에 ~8분 소요된다. 백그라운드 실행 후 회수할 것.
tests/test_sanity.py는 HEAD 이전부터 45초 이상 행(hang) 하는 기존 문제로 본 작업 범위 밖이다 (baseline 아카이브 대조 확인 완료). 게이트에 포함하지 않는다.
12. IMPROVEMENTS.md 갱신 문안
### **B-1: `find_workspace_uuid` tier-3 신원 캐시 해석 오류** — ✅ 해소 (F1/F2)
- tier-3이 상태 파일 경로를 `<workspace>/.mam/` 로 추측하던 문제를 제거하고, 이미 로드된 병합 상태(`d`)를
1차 소스로, `$YAML_PATH`(권위 경로)를 폴백으로 사용하도록 교정.
- PyYAML 부재 시 SQLite 분기까지 무력화되던 `import yaml` 위치, 상태 딕셔너리 `d` 섀도잉,
비정상 `agent_identities` 입력 시 `rc=1`(계약 위반), hermes/cline 폴백 키 오류를 함께 수정.
- **DB가 권위, YAML은 미러**이며 tier-3은 미러를 권위보다 앞세우지 않는다(V-8로 고정).
- 회귀 테스트 `tests/test_b1_tier3_identity.py` (V-1…V-8) 신설.
### **B-10: `agent_identities` 쓰기 경로 부재**
- 저장소 전체에 `agent_identities` 를 생성·갱신하는 코드가 **0건**이며, 읽기(lib.sh tier-3,
reconcile.sh 진단)와 삭제(stop_session.sh --purge-conversation)만 존재한다. 현행 코드로 생성된
워크스페이스에서 tier-3은 구조적으로 빈 값이며, 하위 호환 읽기 경로로만 기능한다.
- 결정 필요: (a) 세션 캡처 시 기록하는 쓰기 경로 신설 / (b) 하위 호환 전용으로 명시하고 문서화.
### **B-11: `load_state_json` 의 PyYAML 하드 의존**
- `lib.sh:511` 이 모듈 최상단에서 `yaml` 을 import 하여, `.db` 만으로 충분한 경우에도 PyYAML 부재 시
traceback 과 함께 상태가 `{}` 로 붕괴되어 tier-1·tier-2 가 동시에 무력화된다.
- `_delegate_py_bin` 이 `$VIRTUAL_ENV` 를 최우선 선택하므로 `deploy/install.sh` 의 시스템 python3
PyYAML 게이트를 우회하는 경로가 실재한다(실측).
총 추적 건수 18 → 19 (B-1 해소, B-10·B-11 신규).
13. 리스크
| ID | 리스크 | 평가 | 완화 |
|---|---|---|---|
| RK-A | 폴백 진입 조건 변경으로 tier-3이 느려진다 | 무시 가능 — 재조회 0.063 ms/call, tier-1·2 실패 시에만 도달 | 실측 기재. 성능을 논거로 쓰지 않음 |
| RK-B (개정) | d에 신원이 있는지로 분기하면 폴백이 상시 경로가 된다(N-1 때문) |
중 — 동작은 동일하나 "재조회 없음" 설계 근거가 소멸 | D2를 "경로 추측 제거"로 축소 기술. V-7이 결합 파손을 CI에서 검출 |
| RK-C | isinstance 가드가 정상 입력을 거른다 |
없음 — dict일 때 무동작 | V-5·V-6 |
| RK-D | F2 치환 시 동일 문자열 2줄을 replace_all로 뭉갠다 |
중 | 문맥 포함 개별 치환 강제 |
| RK-E | 수정 후에도 실사용 복원이 늘지 않아 "미해결"로 오판 | 높음 | N-1(B-10) 동반 보고. 효과 범위를 하위호환 상태를 가진 워크스페이스로 한정해 커밋 메시지에 명시 |
| RK-F (신규) | 후대에 "S2/S6도 살리자"며 YAML 미러 union 폴백을 추가한다 | 중 — --purge-conversation으로 지운 신원 부활, P0-C 격리 훼손 |
V-8이 즉시 red. §2-1 근거를 F4 주석으로 코드 옆에 남긴다 |
| RK-G (신규) | YAML_PATH 미설정 하드닝(변경 ②)이 공개 인터페이스로 검증 불가 |
낮음 | 테스트를 만들지 않고 구조적 방어로만 남긴다고 명시. 검증하지 않은 것을 검증했다고 적지 않음 |
14. 작업 순서 체크리스트 (Creator용)
tests/test_b1_tier3_identity.py추가 → G-B(4 failed, 4 passed) 확인. red 미확인 시 진행 금지.- F1 적용(
lib.sh:1266-1282치환) → G-A, G-C. - F2 적용(
lib.sh:1295,1299개별 치환) → G-C 재확인. - G-E → G-D(백그라운드)로 회귀 0 확인.
- F4 주석/문서 →
IMPROVEMENTS.md§12 문안 반영. - 커밋 분할:
fix(lib): resolve tier-3 identity source from loaded state and $YAML_PATH (B-1)fix(lib): read hermes,cline tier-3 fallback from the agent entrytest(lib): add B-1 tier-3 regression suite (V-1..V-8)docs(improvements): close B-1, open B-10,B-11
15. 경계 선언
본 문서는 설계·리뷰 산출물이며 저장소 코드는 한 줄도 수정하지 않았다. 이의제기 코드·Rev.1·Rev.2
세 변형과 8개 테스트는 전부 스크래치패드의 git archive HEAD 클론 안에서만 실행했고, 작업 트리는
호출 시점과 동일하게 clean이다(b6c41e6). herdr 명령 실행 없음, 라이브 세션 미접촉.
MULTI_AGENT_RULES.md §1에 따라 구현은 Creator, 커밋은 GM 소관이다. 차단 항목 없음.
Creator께: 이의제기의 전제와 처방은 측정으로 기각되었으나, 그 과정에서 잔여 계약 위반(변경 ②)
을 발견했고 V-7·V-8 두 개의 고정 테스트를 얻었습니다. 지적해 주신 결합(if d:의 암묵적 의존)은
실재하며, Rev.2는 이를 런타임 우회 + CI 검출 두 겹으로 막습니다.
[VERDICT: PASS] [AGREEMENT: REACHED]