Review loop (--review-only, --all-reviewer, focused on this Mac: macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@6f4f40a) agy and grok PASS, claude and codex NOT PASS (a non-uuid id `memory` purged the auto-memory dir, a linked dir below a root was followed, a NaN startedAt counted as dead), sent as a PR comment and fixed by the author in eleven commits; round 2 (@2a8d738) confirmed all three fixed but 4/4 NOT PASS on one test holder (`sh -c "sleep 120" <uuid>` loses the uuid on macOS /bin/sh), fixed in de6f3fc; round 3 (@de6f3fc) 4/4 PASS. Full suite on macOS / Python 3.9.6 @de6f3fc: 1934 passed, 56 skipped, 1 error (the known #55 false positive: agy conversations of another workspace). The real ~/.claude, /private/tmp/claude-<uid> and herdr panes unchanged. Also names lib_py.delegation.logroot in eager.CORE_MODULES (the v4.9.4 review's non-blocking note). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
9.7 KiB
9.7 KiB
리뷰 리포트 — PR #59 (Issue #31, claude purge 잔여물) / job 207c65b8
- 리뷰어: planner-reviewer-claude-01 (Opus 5.5)
- 검증 경로:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1 - identity: HEAD
6f4f40a4adb2115d5997dffd31fb6473f05cab68, treef4d9422ad0c1d616b5f37065f3e96b34c28f78d8— 일치. 트리 clean,.mam없음(테스트 실행 후에도 동일). - 범위: base
26e2c43.. head6f4f40a(13 커밋, 12 파일)
요약
설계와 대부분의 구현은 이 Mac에서 맞게 동작한다. 실제 ~/.claude 구조, BSD ps -o lstart=, /tmp → /private/tmp 스크래치패드 레이아웃이 어댑터 가정과 일치하고, 다른 세션·공유 파일·심볼릭 링크 너머는 지우지 않았다. 다만 삭제 범위 가드(_plausible_id)가 UUID가 아닌 id를 거르지 못해, id memory 로 purge 하면 projects/<key>/memory/(Claude Code 자동 메모리 디렉터리)가 통째로 지워진다(F1). 이 PR이 <id>.jsonl 삭제에서 <id> 디렉터리 rmtree 로 범위를 넓히면서 생긴 회귀이고, 브리프 중점 항목("잘못된 uuid 거부")에 해당하므로 NOT PASS 로 판정한다. 수정은 한 줄 수준이다.
차단 결함
F1. 비-UUID id memory 가 자동 메모리 디렉터리를 지운다
- 위치:
.agents/skills/lib_py/agents/base.py:89-92(_plausible_id: 빈 문자열·점만·/·..·*만 거부),.agents/skills/lib_py/agents/adapters/claude.py:262-268(base = projects/<key>/<uuid>, 디렉터리면_remove→shutil.rmtree),claude.py:287-289(has_artifacts가 같은 목록을 써서purge auto도 claude 로 해석). - 실제 이 Mac 의
~/.claude/projects/<key>/에는 세션 디렉터리와 나란히memory/가 있다(읽기만 확인:projects/-Users-godopu16-PuKi-laa-canary-projects-multi-agent-mux/memory/). 세션 id 와 같은 층에 있는 유일한 비-UUID 이름이다. - 재현 1(어댑터 직접, 샌드박스 HOME/CLAUDE_PROJECT_DIR/CLAUDE_CODE_TMPDIR/TMPDIR,
/usr/bin/python33.9.6):has_artifacts("memory") → True,purge_artifacts("memory") → [.../projects/<key>/memory, .../file-history/memory],MEMORY.md survives: False. - 재현 2(CLI):
python -m lib_py.agents purge claude memory <ws>→ 이 호스트에서는 다른 프로세스 29개의 argv 에 "memory" 가 들어 있어 우연히 in-use(rc 4)로 거부됐다. argv 에 "memory" 가 없는 호스트를 흉내 내려고ps출력에서 그 줄만 거르는 스텁을 PATH 앞에 두면 rc 0,"purged": [".../projects/<key>/memory"],memory survives: False. (스크립트:/private/tmp/claude-501/-Users-godopu16-PuKi-laa-canary-projects-multi-agent-mux/8cb38cbf-0d61-4d78-8c7e-065e409ce13d/scratchpad/j207/probe_mem.py) - 도달 경로: 스킬 진입점은 안전하다 —
orc_onboard.sh:88-95은 정규식으로 UUID 를 강제하고,stop_session.sh의PURGE_UUID는find_workspace_uuid가<id>.jsonl의 sessionId 일치로 검증한 값이다. 위험한 경로는 직접 쓰는lib_py.agents purge/어댑터 API(VERSIONS.md 에 문서화)다. 그러나 이 PR 이전에는 같은 입력이 존재하지 않는memory.jsonl만 겨냥해 무해했다. 지워지는 것은 복구할 수 없는 사용자 메모리이고, 테스트(tests/test_issue31_purge_residual.py:242의bad목록)도 이 경우를 다루지 않는다. - 제안: claude 어댑터의
_owned_paths/residual_artifacts(또는purge_artifacts·has_artifacts입구)에서 정규 UUID([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}, 대소문자 무시)만 받게 하고,bad목록에"memory"와 UUID 가 아닌 평범한 이름을 추가하는 회귀 테스트. 다른 어댑터는 이름 스캔(scan_names)·보고만 하므로 공용_plausible_id는 그대로 둬도 된다.
확인한 사항 (정상)
- BSD
ps -o lstart=: 이 Mac 출력은Fri Oct 16 08:25:14 2026(LC_ALL=C 와 기본 로캘 동일)." ".join(out.split())+%a %b %d %H:%M:%S %Y로 파싱된다. 실제 살아 있는 claude 4개의sessions/<pid>.json에서startedAt − lstart= 0.69 / 1.14 / 1.93 / 2.43 s → 모두 live 판정(lstart <= startedAt+2). 샌드박스sleep프로세스로: 방금 쓴 startedAt → in-use rc 4(--wait 2로 2.2 s 대기 후), purge CLI rc 4, jsonl 보존; 프로세스 종료 후 rc 0. startedAt 을 1시간 전으로 둔(pid 재사용) 경우 → free, 그 세션 파일만 삭제.ps실패·빈 출력·파싱 실패·startedAt없음 → live(claude.py:66-74). - tmp 루트: TMPDIR=
/var/folders/.../T/아래에는claude-501이 없고, 실제 스크래치패드는/private/tmp/claude-501/<key>/<uuid>/{scratchpad,tasks}다. 후보에 항상/tmp/claude-<uid>가 들어가고(claude.py:41) realpath 로/private/tmp와 중복 제거된다.<key>는 계산하지 않고 루트 아래 모든 키를 나열해<k>/<uuid>만 고르므로 키 규칙 차이에 영향받지 않는다. 샌드박스에서 CLAUDE_CODE_TMPDIR·TMPDIR 양쪽 루트의<uuid>만 지워졌다. - 실제
~/.claude구조(읽기만):sessions/<pid>.json(intpid, msstartedAt,sessionId) +<pid>.<64hex>.key,jobs/<8hex>/state.json(sessionId[:8] = 디렉터리명),file-history/<uuid>/,session-env/<uuid>/,projects/<key>/<uuid>/{tool-results,…}— 어댑터 가정과 일치. - 삭제 범위(샌드박스,
/usr/bin/python3): 같은 jobs 접두사의 다른 세션, 다른 세션의 sessions json·key, 같은 pid 접두사이지만 64hex 가 아닌.key,history.jsonl, 다른 uuid 의 file-history·jsonl·스크래치패드가 모두 보존됐다.session-env/<U>/link → 외부, 스크래치패드 안의linkout → 외부심볼릭 링크는 링크만 지워지고 대상 파일은 남았다. 재실행 → rc 5(남은 것 없음).""/./../.../a/b/*/../x→ 아무것도 지우지 않음(rc 1 또는 5). - stop exit 4 계약: 게이트(
stop_session.sh:285-300)는 레지스트리 잠금 밖, YAML 쓰기 전에 실행되고 0 이외는 모두 exit 4 + cancelled 이벤트._delegate_py_bin가/usr/bin/python3로 떨어져도-m lib_py.agents in-use는 paho 없이 동작한다(직접 확인). bash 3.2-n통과,inuse_rc=0; … || inuse_rc=$?는set -u에서 안전. - residual 보고: purge 후
residual_artifacts가 빈 목록이면residual: none (agent: …). 실제~/.claude에 대한 이름 스캔(읽기 전용, 가짜 uuid)은 0.02 s. - Python 3.9 / bash 3.2: 변경된 .py 8개 모두
feature_version=(3,9)로 파싱되고, 프로브는 전부/usr/bin/python33.9.6 으로 실행됐다(registry 가 모든 어댑터를 import).lib.sh·stop_session.sh는/bin/bash3.2.57-n통과. - 문서·skill_code_blocks.json: stop SKILL.md 의 "purge 는 행을 지운다"·exit 4·삭제 목록이 코드와 맞고,
test_issue42_skill_blocks.py통과(sha·passthrough 라인 갱신 정합). - codex·grok·opencode residual: 이름 스캔 +
mode=roSQLite 조회만 하므로 쓰기 없음.
테스트
- 검증 경로에서
env -i …(HERDR_·WORKSPACE_ROOT·MAM_·DELEGATE_JOB_LOGS_DIR·CLAUDE_CONFIG_DIR 미설정),--basetemp는 스크래치패드,PYTHONDONTWRITEBYTECODE=1 -p no:cacheprovider:test_issue31_purge_residual, test_issue10_13_purge, test_a4_adapter_contract, test_issue42_skill_blocks, test_issue55_agent_homes, test_orc_onboard, test_uuid_target→ 188 passed, 1 skipped (276 s). 실행 후 트리 clean. - 사전 수정 대조:
git archive HEAD복사본에서 제품 파일 8개를 base26e2c43로 되돌리고test_issue31_purge_residual.py실행 → 24 failed, 8 passed. 통과한 8개는 "다른 세션을 건드리지 않음"·"잘못된 uuid 거부"처럼 이전 코드에서도 성립하는 안전성 테스트로, 기대한 결과다. stop 게이트 테스트(test_stop_purge_refuses_while_the_claude_process_lives)와 pid 재사용 테스트도 사전 코드에서 실패한다.
비차단 메모
- N1. DST:
time.mktime(time.strptime(...))는tm_isdst=-1이라 서머타임이 끝나는 겹치는 1시간에는 lstart 가 1시간 늦게 해석될 수 있고, 그러면 살아 있는 세션을 stale 로 볼 수 있다(fail-open). 이 Mac 은 KST(서머타임 없음)라 영향 없음. 실제 세션 파일에procStart(UTC 문자열,pidDomain: "darwin")가 있으니 이를 쓰거나 비교를 UTC 로 하는 것도 고려할 만하다. - N2. stop 의 exit 4 메시지(
stop_session.sh:296)는~/.claude/sessions/만 가리킨다. CLAUDE_CONFIG_DIR 을 쓰면 위치가 다르고, argv 에 uuid 를 담은 아무 프로세스(에디터·grep·위임 래퍼)도 거부 사유가 된다. orc_onboard SKILL.md 처럼ps -axww | grep <uuid>안내를 함께 두면 좋다. - N3. SKILL.md exit 4 행의 "(nothing was changed)" — 프로세스 게이트는 herdr kill 이후에 돌므로 세션은 이미 내려가 있을 수 있다(레코드는 그대로). 기존 herdr 게이트 문구와 같은 뉘앙스이지만 "records preserved" 가 더 정확하다.
- N4. stop 이 이제 모든 에이전트에 in-use 게이트를 적용한다. agy 처럼 CLI 종료 뒤에도 다른 프로세스가 DB/락을 열어 두는 구성이면 이전에는 purge 되던 것이 exit 4 로 바뀐다. 의도된 fail-closed 이지만 실제 agy 로는 확인하지 못했다(브리프상 실제 에이전트 실행 금지).
- N5. 테스트는 하드코딩된
/tmp/claude-<uid>후보를 격리하지 못한다(실제/private/tmp/claude-501을 나열). 무작위 uuid 라 실제로 지워지는 것은 없지만,_tmp_roots를 테스트에서 패치할 수 있게 하면 더 안전하다.
[VERDICT: NOT PASS]