release(v4.9.5): a claude purge removes every path the session owns (Issue #31, PR #59) - archive the review reports, bump the lockstep version, load logroot eagerly
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
This commit is contained in:
1 parent
de6f3fc81f
commit
6fd417a7b7
32 files changed
+916
-26
No files matched your search
@@ -0,0 +1,128 @@
|
||||
# PR #59 Mac review — NOT PASS
|
||||
|
||||
Job `05572944`, role Reviewer. 소스 수정 없이 검토했다. 선택 테스트 총 **80 passed, 1 skipped**이지만, 추가 탐침에서 삭제 범위 문제 두 건과 live-process 판정 문제 한 건을 확인했다. 병합 전 수정이 필요하다. 국소적인 안전성 수정 및 회귀 테스트로 해결 가능하므로 재계획 에스컬레이션은 하지 않는다.
|
||||
|
||||
## Review identity
|
||||
|
||||
- loop: `loop-20261016T003524Z-a51548`
|
||||
- base: `26e2c43753169671b600d03274901dbe203988dd`
|
||||
- head: `6f4f40a4adb2115d5997dffd31fb6473f05cab68`
|
||||
- tree: `f4d9422ad0c1d616b5f37065f3e96b34c28f78d8`
|
||||
- 검증 트리: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1`
|
||||
- 범위: base..head 13 commits / 12 files. 아래 파일:라인은 이 head 기준이다.
|
||||
- PR #59 본문과 issue #31을 tea의 읽기 명령으로 확인했다.
|
||||
|
||||
## R1 — [P1] 상위 디렉터리의 심볼릭 링크를 따라 소유 루트 밖을 삭제한다
|
||||
|
||||
근거:
|
||||
|
||||
- `.agents/skills/lib_py/agents/adapters/claude.py:279`–`:284`: tmp root의 모든 entry를 key로 간주하고 `root/key/uuid`를 후보에 넣는다. key가 외부 디렉터리를 가리키는 링크인지 검사하지 않는다.
|
||||
- 같은 파일 `:80`–`:85`: `_remove`는 최종 path 자체가 링크인지 확인하지만, 상위 key가 링크이면 최종 uuid 디렉터리는 일반 디렉터리로 보인다. 따라서 `shutil.rmtree`가 링크 너머 실제 디렉터리를 삭제한다.
|
||||
- 같은 파일 `:266`–`:270`의 projects/file-history/session-env 경로에도 상위 구성요소 경계 검증은 없다.
|
||||
|
||||
재현(실제 홈/스크래치패드는 사용하지 않음):
|
||||
|
||||
1. 임시 config/HOME/workspace/tmp를 만들고 새 UUID U를 생성한다.
|
||||
2. 임시 소유 루트 밖인 `<review-temp>/outside-owned-roots/U/keep.txt`를 만든다. 이는 테스트용 sentinel이며 실제 사용자 파일이 아니다.
|
||||
3. 임시 `<ctmp>/claude-<uid>/linked-workspace`를 `outside-owned-roots`에 연결한다.
|
||||
4. `_tmp_roots`를 이 임시 claude root 하나로 제한하고 `ClaudeAgentAdapter.purge_artifacts(U, ctx)`를 실행한다. 제한은 실제 `/tmp/claude-<uid>`에 접근하지 않도록 하는 시험 장치이며 경로 열거/삭제 로직은 변경하지 않는다.
|
||||
|
||||
기대: 소유 경계 밖 sentinel은 보존되어야 한다. 최소한 링크를 경유한 후보를 거부하고 residual/경고로 남겨야 한다.
|
||||
|
||||
실제:
|
||||
|
||||
```text
|
||||
ANCESTOR_SYMLINK {"victim_exists": false, "link_survives": true,
|
||||
"purged": [".../ctmp/claude-501/linked-workspace/<U>"]}
|
||||
```
|
||||
|
||||
상위 링크 자체는 남고 링크 너머 디렉터리가 삭제됐다. 최종 UUID entry 자체를 symlink로 만든 대조군에서는 target이 살아 있고 링크만 삭제됐다. 즉 leaf symlink 보호만으로 해결되지 않는 문제다.
|
||||
|
||||
수정 방향: 명시적으로 설정한 root를 신뢰 경계로 정규화한 뒤, 그 아래 탐색 중 만난 symlink 디렉터리를 따라가지 않는다. tmp의 workspace key 등 중간 경로도 `lstat`/no-follow로 확인해야 한다. 경계 밖 realpath 후보는 삭제하지 말고 보고한다. 링크 교체 경합까지 고려하면 디렉터리 descriptor와 no-follow 연산으로 안전하게 경로를 여는 방식이 적절하다. leaf-link 대조군과 ancestor-link 탈출 회귀 테스트를 둘 다 추가한다.
|
||||
|
||||
## R2 — [P2] UUID가 아닌 `memory`를 세션으로 받아 공유 메모리 디렉터리를 삭제한다
|
||||
|
||||
근거:
|
||||
|
||||
- `.agents/skills/lib_py/agents/base.py:89`–`:92`: `_plausible_id`는 일부 경로 문자만 배제한다. `memory`, 공백, 임의 문자열은 UUID가 아니어도 통과한다.
|
||||
- `.agents/skills/lib_py/agents/adapters/claude.py:262`–`:268`: 이 검증만으로 `projects/<key>/<입력>` 디렉터리 전체를 소유 경로로 간주한다.
|
||||
- 같은 파일 `:287` 및 `:296`: `has_artifacts`와 purge가 같은 오판을 공유한다. 기존 코드의 단일 `<uuid>.jsonl` 삭제와 달리 이제 디렉터리 전체 삭제로 이어진다.
|
||||
|
||||
재현:
|
||||
|
||||
1. 임시 config의 `projects/<workspace_key>/memory/MEMORY.md`에 공유 메모리 sentinel을 만든다.
|
||||
2. `adapter.has_artifacts("memory", ctx)`를 호출한다.
|
||||
3. `adapter.purge_artifacts("memory", ctx)`를 호출한다.
|
||||
|
||||
기대: Claude 세션 UUID가 아닌 입력을 거부하고 memory를 보존해야 한다.
|
||||
|
||||
실제:
|
||||
|
||||
```text
|
||||
MEMORY_ACCEPTED True
|
||||
NON_UUID {"memory_exists": false,
|
||||
"purged": [".../home/.claude/projects/<workspace_key>/memory"]}
|
||||
```
|
||||
|
||||
현재 malformed-id 테스트는 빈 값, 점, 별표, slash만 다루므로 이 경우를 검출하지 않는다. 정상 UUID를 purge할 때 memory를 보존하는 테스트가 통과하는 것과는 별개다.
|
||||
|
||||
한계/구분: 이 호스트에서 실행한 **별도의 무수정 CLI** `purge claude memory` 탐침은 일반 `conversation_in_use`의 프로세스 문자열 검사에 걸려 rc 4로 끝났고 memory를 보존했다. 따라서 CLI가 이 실행에서 memory를 삭제했다고 주장하지 않는다. 위 삭제는 어댑터 호출에서 직접 재현했다. 프로세스 목록에 우연히 `memory`가 등장하는지 여부는 UUID 검증이나 공유 파일 소유권 보장의 대안이 아니며, CLI target 탐색도 이 잘못된 `has_artifacts`에 의존한다.
|
||||
|
||||
수정 방향: Claude의 디렉터리 삭제 후보를 만들기 전에 Claude 세션 식별자 형식을 검증한다(예: UUID 파싱 및 허용된 표기 검증). 다른 어댑터의 `ses_...` 식별자까지 전역적으로 금지하지 말고 Claude에 맞는 검증을 적용한다. `memory`, 공백, 임의 비-UUID 문자열은 has/purge에서 거부하고 실제 공유 memory 파일이 살아 있음을 회귀 테스트로 고정한다.
|
||||
|
||||
## R3 — [P2] NaN startedAt을 판정 불가로 처리하지 않아 live guard가 열린다
|
||||
|
||||
근거:
|
||||
|
||||
- `.agents/skills/lib_py/agents/adapters/claude.py:70`: `float(data["startedAt"])`가 `"NaN"`과 JSON NaN을 예외 없이 변환한다.
|
||||
- 같은 파일 `:73`, `:77`: NaN은 `<= 0`도 거짓이고 `lstart <= started + 2`도 거짓이므로, 살아 있는 PID에 대해 False를 반환한다.
|
||||
- 같은 파일 `:275`: 그 세션 파일은 죽은 프로세스의 소유 경로로 선택된다.
|
||||
- `.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh:292`–`:298`: in-use rc 0이면 purge gate를 통과한다.
|
||||
|
||||
재현:
|
||||
|
||||
1. 실제 Claude 대신 `/bin/sleep 60`을 띄운다.
|
||||
2. 임시 config의 `sessions/<sleep-pid>.json`에 새 UUID, 해당 live pid, `"startedAt": "NaN"`을 기록한다.
|
||||
3. 모든 Claude/home/tmp 환경을 임시 위치로 지정한 CLI `python -m lib_py.agents in-use claude <UUID> <temp-workspace>`를 실행한다.
|
||||
|
||||
기대: 시작 시각을 비교할 수 없으므로 보수적으로 live=True, CLI rc 4.
|
||||
|
||||
실제:
|
||||
|
||||
```text
|
||||
CLI_NAN {"rc": 0, "stdout": "{\"agent\": \"claude\", \"in_use\": false}\n",
|
||||
"holder_alive": true, "session_file_exists": true}
|
||||
```
|
||||
|
||||
직접 함수 탐침에서도 string NaN과 JSON NaN은 둘 다 False였고, `_owned_paths`에는 이 live PID의 세션 파일이 포함됐다. live fixture를 실제로 purge하지는 않았다.
|
||||
|
||||
수정 방향: startedAt 변환 후 `math.isfinite` 및 유효 범위를 확인하고, 비유한 값이면 True(live/unknown)를 반환한다. 필요하면 bool 등 시각이 아닌 타입도 배제한다. NaN/무한대/결손/파싱 불가 입력과 정상 live/dead/reused PID를 함께 검사한다.
|
||||
|
||||
## Mac 검증 및 통과한 범위
|
||||
|
||||
- `tests/test_issue31_purge_residual.py`: **32 passed in 23.89s**, rc 0.
|
||||
- `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`: **48 passed, 1 skipped in 1.48s**, rc 0. skip은 리뷰 worktree에 untracked `.claude/skills`가 없는 경우이다.
|
||||
- base `26e2c43`의 임시 archive 복사본에 새 테스트 파일을 올려 핵심 3건을 실행: `test_claude_purge_removes_every_owned_path`, `test_claude_residual_lists_leftovers_then_nothing`, `test_purge_cli_refuses_a_live_claude_without_uuid_on_argv` 모두 예상대로 실패. 현재 head에서는 통과한다. 전체 32건의 base 비교를 다시 수행한 것은 아니다.
|
||||
- 실제 `/usr/bin/python3` 3.9.6에서 변경 Python 8개 AST 파싱 통과. 변경 shell 2개를 각각 `/bin/bash -n`으로 검사하여 통과. 실제 Bash 3.2로 stop/in-use fixture 테스트 실행.
|
||||
- 실제 BSD `/bin/ps -p <sleep-pid> -o lstart=` + LC_ALL=C 출력 예: `Fri Oct 16 10:18:16 2026`. 정상 현재 startedAt은 live=True, 한 시간 이전 startedAt은 PID 재사용으로 판정되어 live=False, 결손은 unknown/live=True. 파싱 불가 ps 출력의 보수적 처리는 신규 테스트로 통과했다. NaN은 위 별도 결함이다.
|
||||
- 정상 UUID의 다른 세션/동일 jobs 접두사의 다른 세션/shared history 보존, stale session/key 삭제, 부분 삭제 실패의 소유 증거 유지, CLAUDE_CONFIG_DIR, stop rc 4 및 행 보존/삭제, codex/grok/opencode residual은 새 테스트 범위에서 통과했다.
|
||||
- stop SKILL.md의 row 제거 설명과 `skill_code_blocks.json` 변경은 코드/문서 검사와 일치했다.
|
||||
- `git diff --check base HEAD` 통과. 검증 후 HEAD/tree 일치 및 clean git status 확인.
|
||||
|
||||
## 실제 구조 관찰과 격리 범위
|
||||
|
||||
실제 홈에서는 디렉터리 목록/파일 종류/존재 여부만 읽었다. JSON, key, transcript 내용은 열지 않았다. 관찰 당시 `~/.claude`에는 projects 16개 디렉터리, file-history 18개, session-env 21개, sessions의 JSON 5개/key 5개가 있었다. jobs의 표본 디렉터리 두 곳에 state.json이 존재했다. `/private/tmp/claude-501`에는 workspace 디렉터리 5개가 있었고 표본에서 `<key>/<session>/scratchpad` 구조를 확인했다. `/tmp`의 realpath는 `/private/tmp`, 이 Mac의 TMPDIR realpath는 `/private/var/folders/.../T`였다. 이 관찰은 디렉터리 형태를 확인한 것이며 실제 session JSON 필드 값을 검증했다는 뜻은 아니다.
|
||||
|
||||
브리프의 금지 범위를 지키기 위해 테스트는 다음과 같이 격리했다.
|
||||
|
||||
- 지정 `env -u` 목록(HERDR/WORKSPACE_ROOT/MAM/DELEGATE/CLAUDE_CONFIG_DIR)을 적용하고, HOME/HOME_DIR 및 Claude 프로젝트·tmp 경로를 임시 위치로 지정했다. config 기본값 테스트는 임시 HOME의 `.claude`를 사용하며 custom config 테스트도 임시 경로만 사용했다.
|
||||
- 코드가 명시적 임시 환경과 무관하게 `/tmp/claude-<uid>`도 탐색하므로, 테스트용 sitecustomize에서 **그 실제 fallback root에 대한 isdir만 False로 처리**했다. 그 실제 root에 대한 remove/rmdir/rmtree/rename도 방어적으로 차단했다. 이 테스트 보조 파일은 임시 디렉터리에만 있고 제품 소스는 변경하지 않았다.
|
||||
- 따라서 tmp root 단위 테스트는 실제 fallback을 제외한 격리 환경에서 통과한 것이다. 실제 fallback의 존재/레이아웃은 위 읽기 관찰로 별도 확인했다.
|
||||
- 수동 purge 탐침의 `_tmp_roots`는 임시 root 하나로 제한했다. CLI 탐침도 동일한 real-fallback 차단을 적용했다. 삭제 sentinel은 전부 리뷰 임시 디렉터리 안의 합성 데이터다.
|
||||
- 실제 Claude 프로세스, herdr 서버, monitor는 시작하지 않았다. 실제 홈/실제 scratchpad/실제 agent session에 purge 또는 in-use를 실행하지 않았다. 제어 평면 쓰기는 지시된 lifecycle 이벤트와 본 리포트뿐이다.
|
||||
- 모든 pytest 실행에 지정 venv Python, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, 검증 트리 밖 basetemp를 사용했다. creator 트리에서 실행하지 않았다.
|
||||
- 전체 스위트는 오케스트레이터의 별도 실행 범위이며 이번 리뷰에서 재실행하지 않았다. 선택 테스트에서는 #55 오탐/teardown 오류가 없었다.
|
||||
|
||||
산출물: `/tmp/review-05572944-u3lt5wk_/`의 `pytest.log`, `docs-home.log`, `before.log`, `probes.py`, `probes.log`, `cli-probe.py`, `cli-probe.log`, `guard/sitecustomize.py`. 임시 스크립트는 해당 검증 트리 cwd에서 실행하도록 작성됐으며 모든 파괴적 동작은 자체 임시 환경을 만든 후 수행한다.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,76 @@
|
||||
# PR #59 2차 Mac 리뷰 — NOT PASS
|
||||
|
||||
- Job: `a59e709d`; loop: `loop-20261017T135731Z-e93f0a`
|
||||
- Base: `26e2c43753169671b600d03274901dbe203988dd`
|
||||
- Head: `2a8d73825b3832376aeaf96b09548359dc3cb887`
|
||||
- Tree: `d146f1ac68bcc3485d24fb92edb87264e4d1e8cb`
|
||||
- 검증 트리: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1`
|
||||
- 환경: macOS, 시스템 Python 3.9.6, bash 3.2, BSD ps. pytest는 지정된 제어 저장소의 `.venv/bin/python`으로 실행.
|
||||
|
||||
기존 3건의 결함은 이번 HEAD에서 해결됐고 수정 전 실패도 확인했다. 다만 새 UUID 대소문자 회귀 테스트 2건이 macOS에서 실패한다. 원인은 제품의 UUID 정규화가 아니라 테스트가 UUID 없는 프로세스를 생성하는 데 있다. 현재 HEAD를 이 Mac에서 통과한 상태로 승인할 수 없으므로 NOT PASS다. 수정 범위는 테스트 픽스처이며 재계획은 필요하지 않다.
|
||||
|
||||
## 수정 필요: [P2] UUID 명령행 보유자 픽스처가 macOS에서 UUID를 잃음
|
||||
|
||||
**근거:** `tests/test_issue31_purge_residual.py:315`의 `subprocess.Popen(["sh", "-c", "sleep 120", conv])`, 이어지는 `:318`의 live 단언. `upper`와 `mixed` 두 매개변수에서 동일하게 실패한다.
|
||||
|
||||
macOS의 sh는 마지막 명령을 실행하면서 동일 PID의 명령행을 `sleep 120`으로 바꾼다. `conv`는 셸의 `$0`였으며 sleep 인자로 전달되지 않는다. 앞서 `live.kill()` / `live.wait()`로 세션 파일의 PID도 죽였기 때문에 이 시점에는 세션 파일이나 명령행 어디에도 살아 있는 보유자가 없다. 어댑터가 False를 돌려주는 것은 이 픽스처 상태에서 올바르다.
|
||||
|
||||
**재현 절차:**
|
||||
|
||||
1. 검증 트리에서 아래 테스트 노드를 실행한다. 임시 HOME/HOME_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CODE_TMPDIR, TMPDIR를 사용하고 브리프의 HERDR/MAM/WORKSPACE 환경 변수들을 제거한다. 테스트의 `home` 픽스처는 CLAUDE_CONFIG_DIR를 제거하여 임시 HOME/.claude를 쓰며, `own_tmp_roots`는 고정 /tmp fallback까지 임시 경로로 대체한다.
|
||||
|
||||
```sh
|
||||
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python -m pytest -p no:cacheprovider --basetemp=<검증트리밖의새임시경로> -q tests/test_issue31_purge_residual.py::test_any_spelling_of_the_uuid_is_the_lowercase_session
|
||||
```
|
||||
|
||||
2. 독립적으로 새 UUID를 생성하고 `Popen(["sh", "-c", "sleep 120", uuid])`를 띄운 뒤 0.1초 후 `ps -p <그 PID> -o args=`를 읽는다. 관측 결과는 `sleep 120`으로 UUID가 없다. 종료 시 생성한 프로세스만 kill/wait한다.
|
||||
3. 검증 트리 HEAD의 git archive 임시 복사본에서 해당 한 줄만 `Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])`로 대체하고 같은 두 테스트를 실행한다.
|
||||
|
||||
**기대/실제:** 원래 테스트는 lowercase UUID를 명령행에 가진 살아 있는 프로세스를 만들고 upper/mixed ID 모두 in-use=True임을 검증해야 한다. 실제로는 UUID 없는 sleep을 만들어 두 건 모두 `:318`에서 실패했다. 임시 복사본의 한 줄 대체 대조군에서는 **2 passed in 2.36s**, 종료 코드 0이었다. 별도 ps 확인에서도 Python 프로세스는 UUID 인자를 유지했다.
|
||||
|
||||
**수정 방향:** 셸의 마지막 명령 최적화에 의존하지 않는 프로세스를 생성한다. 이미 import한 `sys.executable`을 사용한 위 Python sleep 픽스처가 최소 수정이다. 생성 PID의 명령행에 UUID가 실제로 남아 있는지 확인하고 기존 finally의 kill/wait 정리는 유지한다. 원본 검증 트리나 creator 트리는 수정하지 않았다.
|
||||
|
||||
## 기존 지적의 해결 및 수정 전 실패 검증
|
||||
|
||||
현재 테스트 파일을 각 수정 직전 커밋의 격리된 git archive 복사본에 놓고 해당 노드를 실행했다. 테스트 임시 루트 격리 픽스처도 함께 사용했다. 이는 예전 테스트가 아니라 새 회귀 단언이 예전 구현을 실제로 거부하는지 확인하기 위한 것이다.
|
||||
|
||||
| 기존 지적 | 현재 구현 및 HEAD 결과 | 수정 전 결과 |
|
||||
| --- | --- | --- |
|
||||
| UUID 아닌 `memory`가 공유 폴더를 삭제 | `claude.py:10` 정규 UUID 확인/소문자 변환, `:301` 후보 경로 구성 및 `:368`, `:390` purge/residual 진입 가드. malformed/non-UUID 보존 테스트 통과. upper/mixed의 세션 파일 기반 in-use 및 CLI exit 4 단계도 통과 | `bd09c6a^`에서 `test_claude_rejects_malformed_uuid[memory]` 실패: 기대 []와 달리 memory 관련 경로 3개 삭제 |
|
||||
| 상위 심볼릭 링크를 통한 범위 밖 삭제 | `claude.py:105`의 `_inside`가 루트 아래 상위 구성요소를 lstat하고 `:336`의 `_owned_paths`가 필터링. tmp key/project key/file-history 링크 뒤 파일 보존, leaf 링크만 삭제, 루트 자체 링크 허용 테스트 통과 | `6e0361c^`에서 `test_claude_purge_never_follows_a_linked_dir_below_a_root` 실패: 링크 뒤 경로 4개 삭제 |
|
||||
| NaN/inf/과대 정수 startedAt이 죽음으로 판정 | `claude.py:69`의 `_session_file_live`: bool/type/finite/positive 검사 및 float 변환 OverflowError 처리로 판정 불가를 live로 유지. 숫자 경계값 및 CLI 테스트 통과 | `f943b0d^`에서 `[NaN]` 테스트 실패: 살아 있는 PID인데 False. 추가로 `60779ba^`에서 `[10**400]` 테스트 실패: OverflowError |
|
||||
|
||||
각 수정 전 실행은 1 failed였으며, 동일 노드들이 현재 HEAD의 관련 모듈 실행에서는 통과했다. 기존 3건을 계속 미해결인 제품 결함으로 보고하는 것은 아니다.
|
||||
|
||||
## 검증 결과와 범위
|
||||
|
||||
다음 네 모듈을 함께 실행: `tests/test_issue31_purge_residual.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`, `tests/test_a4_adapter_contract.py`.
|
||||
|
||||
- 원본 HEAD: **146 passed, 2 failed, 1 skipped in 37.80s**. 위 P2의 upper/mixed 두 건만 실패. 전체 스위트는 실행하지 않았다.
|
||||
- 임시 복사본의 픽스처 대조군: 위 두 건만 **2 passed**. 이를 원본 HEAD 전체 통과로 합산하지 않는다.
|
||||
- 변경 Python 파일의 시스템 Python 3.9 구문 검사, 변경 셸 파일의 bash 구문 검사, `git diff --check` 통과.
|
||||
- 종료 전 HEAD/tree 재확인 일치, 검증 트리 `git status --short` 출력 없음.
|
||||
- 실제 Claude 실행/실제 세션 purge·stop·in-use는 수행하지 않았다. destructive 테스트는 임시 홈/프로젝트/config/tmp에서만 실행. issue31 외 모듈에도 실제 `/tmp/claude-<uid>`에 접근하지 않도록 별도 테스트 프로세스용 가드를 적용했다.
|
||||
|
||||
## 추가 검토 사항
|
||||
|
||||
- BSD ps 시작 시각: `LC_ALL=C`, `TZ=UTC0`로 읽어 `calendar.timegm`과 epoch-ms를 비교한다. 실제 임시 sleep 프로세스의 live 판정, 재사용 PID 모형, 판정 불가, UTC/DST 회귀 테스트를 확인했다. startedAt+2초 비교는 유지됐다.
|
||||
- tmp 레이아웃: 실제 경로는 디렉터리 이름/종류 메타데이터만 확인했다. 이 Mac의 `/tmp`는 `/private/tmp`로 연결되고 실제 per-uid scratchpad는 workspace key 아래 UUID 디렉터리 구조였다. TMPDIR는 별도의 `/var/folders/.../T/`로 해석된다. override/base/per-uid/realpath 중복 제거 테스트 통과.
|
||||
- 실제 ~/.claude의 projects, file-history, session-env, sessions, jobs 디렉터리 구조는 구현이 가정한 배치와 부합했다. 실제 세션 파일 및 key 내용은 읽지 않았다. 관측된 metadata 수: projects 디렉터리 16개, file-history 19개, session-env 21개, sessions JSON 6개와 key 6개, jobs 디렉터리 2개와 JSON 파일 1개. 이는 구조 확인이지 모든 실제 파일의 소유권을 검증했다는 뜻은 아니다.
|
||||
- `dd292c1`: 삭제 가능한 `_owned_paths` 대신 전체 `_candidates`로 has_artifacts가 Claude를 선택하므로, 링크 뒤 잔여물만 있어도 auto purge는 Claude로 선택하여 보존 및 residual을 보고한다. `test_purge_cli_reports_a_linked_orphan_it_will_not_remove` 통과.
|
||||
- 공유 파일/다른 UUID scratchpad/동일 접두사 jobs/다른 세션 metadata 보존 및 실패 후 ownership evidence 유지 검증을 관련 모듈에서 확인했다. key 경로는 보고 시 hex를 마스킹한다.
|
||||
- `base.py:311`, `__main__.py:186`, `stop_session.sh:292`의 안내 전달을 검토했다. stdout JSON과 stderr 힌트를 분리하고 stop은 in-use gate의 실패를 exit 4로 처리한다. exit 4 및 purge 시 registry 행 삭제 설명이 stop SKILL에 반영됐다. skill code block 관련 검사 통과.
|
||||
- agy에도 in-use gate가 적용되는 점은 공통 어댑터 계약의 의도된 동작이며 이 자체를 결함으로 판정하지 않았다.
|
||||
- `test_tests_never_reach_the_real_tmp_root` 및 자식 Python의 fallback 주입 검증 통과. #55 관련 외부 agy 아티팩트를 결함 근거로 사용하지 않았다.
|
||||
- 상위 경로 링크 검사는 검사 시점의 디렉터리 구조에 관한 보장이다. 동시 파일시스템 변경을 포함한 원자적 삭제 보장까지 검증한 것은 아니다.
|
||||
|
||||
## 로컬 증거
|
||||
|
||||
임시 증거 루트: `/tmp/review-a59e709d-4yb9c6z4` (macOS 실경로 `/private/tmp/...`).
|
||||
|
||||
- `head.log`: 원본 HEAD 네 모듈 실행 결과.
|
||||
- `before-id.log`, `before-links.log`, `before-nan.log`, `before-overflow.log`: 수정 전 실패 결과.
|
||||
- `fixture-control.log`: 한 줄 픽스처 대체 대조군의 2건 통과.
|
||||
- `fixture-control/`: HEAD archive의 임시 대조군. 제품 코드 변경 없음.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20261017T145452Z-6c5aa6",
|
||||
"base_sha": "26e2c43753169671b600d03274901dbe203988dd",
|
||||
"head_sha": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"tree_sha": "ffc291e6fc93822070f2b9f1d17009e83f176fcf",
|
||||
"review_commit": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"reviewer": "planner-reviewer-codex-01",
|
||||
"job_id": "d5e47fc2",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-17T15:16:54Z"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# PR #59 3차 Mac 리뷰
|
||||
|
||||
판정: **PASS**. base..head 누적 변경에 대해 병합을 막을 추가 결함을 발견하지 않았다. 2차 리뷰에서 남았던 macOS UUID 명령행 보유자 테스트 실패가 해결됐으며, 수정한 테스트가 정규화 회귀를 실제로 검출하는 것도 확인했다.
|
||||
|
||||
## 리뷰 identity
|
||||
|
||||
- Job: `d5e47fc2`
|
||||
- Loop: `loop-20261017T145452Z-6c5aa6`
|
||||
- Base: `26e2c43753169671b600d03274901dbe203988dd`
|
||||
- Head: `de6f3fc81f8c33a9d2335320742dc5a47f566cda`
|
||||
- Tree: `ffc291e6fc93822070f2b9f1d17009e83f176fcf`
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1`
|
||||
- macOS / 시스템 Python 3.9.6 / bash 3.2 / BSD ps. pytest는 지정된 제어 저장소 `.venv/bin/python` 사용.
|
||||
|
||||
시작과 종료 시 HEAD/tree가 브리프와 일치했고 `git status --short` 출력은 없었다. creator 트리에서 실행하거나 수정하지 않았다. 제품 코드, 원본 테스트, 브랜치 및 커밋 변경 없음. 회귀 대조군만 검증 HEAD의 별도 임시 archive 복사본에서 변경했다.
|
||||
|
||||
## 이번 수정 검증
|
||||
|
||||
`git diff 2a8d738..HEAD`는 `tests/test_issue31_purge_residual.py`의 프로세스 생성 한 줄 변경과 설명 주석 두 줄뿐이다. `.agents` 제품 코드와 문서, 다른 테스트 및 공통 fixture는 이전 리뷰 HEAD와 동일하다.
|
||||
|
||||
- `tests/test_issue31_purge_residual.py:317`: `[sys.executable, "-c", "import time; time.sleep(120)", conv]`로 UUID를 Python argv에 남긴다. macOS sh의 마지막 명령 exec 최적화를 피하는 적절한 최소 수정이다.
|
||||
- 독립 확인에서 새 UUID로 동일한 Python 프로세스를 만들고 0.05초 뒤 BSD `ps -p <pid> -o args=`를 읽었다. 명령행 끝에 lowercase UUID가 유지됐다.
|
||||
- `:320`의 보유자 단언은 그대로이며 `:321`~`:323`의 finally kill/wait도 유지됐다. 독립 프로브에서 kill/wait 후 반환 코드 -9, 해당 PID의 ps 출력 없음으로 정리를 확인했다.
|
||||
- 원본 HEAD의 upper/mixed 테스트는 둘 다 통과했다. 세션 metadata 기반 live 및 CLI exit 4, 그 프로세스를 죽인 후 명령행 기반 live, 이후 purge/잔여물 없음까지 기존 검증을 그대로 수행한다.
|
||||
|
||||
### 회귀 검출 대조군
|
||||
|
||||
검증 HEAD를 `/tmp/review-d5e47fc2-oqyptnum/mutation`으로 git archive하고, **제품 원본이 아닌 복사본에서만** `.agents/skills/lib_py/agents/adapters/claude.py:354`의 `for u in {canon, uuid}`를 `for u in {uuid}`로 바꿨다. 세션 파일 조회의 정규화 및 다른 코드는 유지해 명령행 소문자 검색만 제거했다.
|
||||
|
||||
동일 테스트 노드 `tests/test_issue31_purge_residual.py::test_any_spelling_of_the_uuid_is_the_lowercase_session`를 실행한 결과 **2 failed in 1.52s**, pytest 종료 코드 1. upper/mixed 모두 `:320`의 `a command line naming the lowercase id` 단언에서 실패했다. 그 앞의 세션 파일 기반 판정 및 CLI exit 4는 통과했다. 따라서 이번 픽스처 변경은 단언을 약화하지 않으며, 정확히 명령행 UUID 철자 처리 회귀를 검출한다. 이 의도적인 실패는 원본 HEAD의 실패가 아니다.
|
||||
|
||||
## 실행 결과
|
||||
|
||||
원본 HEAD에서 다음 네 모듈을 함께 실행했다.
|
||||
|
||||
- `tests/test_issue31_purge_residual.py`
|
||||
- `tests/test_issue42_skill_blocks.py`
|
||||
- `tests/test_issue55_agent_homes.py`
|
||||
- `tests/test_a4_adapter_contract.py`
|
||||
|
||||
결과: **148 passed, 1 skipped in 38.52s**, pytest 종료 코드 0. 전체 스위트는 브리프대로 오케스트레이터 범위이며 이 리뷰에서 전체 통과를 주장하지 않는다.
|
||||
|
||||
실행 형식은 `PYTHONDONTWRITEBYTECODE=1 <제어저장소>/.venv/bin/python -m pytest -p no:cacheprovider --basetemp=/tmp/review-d5e47fc2-oqyptnum/pytest-head -q <위 네 모듈>`이다. 테스트 실행 전 HERDR/MAM/WORKSPACE/DELEGATE_JOB 바인딩 및 CLAUDE_CONFIG_DIR를 제거하고 HOME/HOME_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CODE_TMPDIR, TMPDIR를 임시 경로로 설정했다. 실제 config 대신 임시 HOME/.claude 또는 각 테스트가 지정하는 임시 config만 사용한다. issue31의 own_tmp_roots fixture는 자식 Python까지 고정 /tmp fallback을 임시화한다. 다른 모듈에는 테스트 프로세스 전용 sitecustomize로 실제 `/tmp/claude-<uid>` 후보를 제외하는 추가 가드를 적용했다.
|
||||
|
||||
- base..head `git diff --check` 통과.
|
||||
- 변경 Python 파일 모두 시스템 Python 3.9의 AST 구문 검사 통과.
|
||||
- 변경 shell 파일 모두 `/bin/bash -n` 통과.
|
||||
- 실제 Claude나 다른 실제 에이전트를 실행/중지/purge하지 않았다. 실제 세션에 대한 in-use 호출도 없었다. 실제 .mam/.mam.env/monitor/herdr 설정을 변경하지 않았다. 지정된 잡 이벤트와 보고서만 제어 평면에 기록한다.
|
||||
|
||||
## 누적 변경 재확인 및 이전 리뷰 근거
|
||||
|
||||
2차 리뷰 리포트: `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.mam/jobs/a59e709d/codex-reports/report-final.md`.
|
||||
|
||||
이번 제품 코드가 그 HEAD와 동일함을 diff로 확인했다. 누적 diff의 공통 base/CLI/stop 변경과 Claude 소유 경로 선택·삭제 흐름을 재검토하고 관련 테스트를 재실행했다.
|
||||
|
||||
- 잘못된 UUID로 memory 등 공유 폴더에 도달하는 문제: canonical UUID 가드 및 소문자 경로 처리 유지. malformed/non-UUID 보존 테스트 통과.
|
||||
- 상위 링크를 통한 범위 밖 삭제: `_inside`의 lstat 검사와 `_owned_paths` 필터 유지. 링크 뒤 잔여물 보존·보고, leaf 링크만 제거, 루트 자체 링크 허용 테스트 통과.
|
||||
- NaN/inf/float 범위 초과 startedAt: 유효 시각이 아니면 live로 처리하는 가드 유지. 관련 숫자 경계와 CLI 테스트 통과.
|
||||
- 기존 세 결함의 수정 전 실패 검증은 2차 리뷰에서 수행한 결과를 재사용한다. memory/상위 링크/NaN 및 추가 OverflowError 회귀 테스트가 각 수정 직전 코드에서 실패했다. 이번에는 그 옛 커밋 테스트를 반복하지 않았다.
|
||||
- BSD ps/UTC 시작 시각 비교, PID 재사용 판별, 판정 불가 시 live 처리, tmp root 우선순위와 realpath 중복 제거, 소유권 state.json을 통한 jobs 선택, 다른 세션/공유 파일 보존 테스트 통과.
|
||||
- `dd292c1`의 링크 뒤 orphan만 남은 경우도 Claude로 선택되어 residual로 보고하는 테스트 통과.
|
||||
- `base.py`의 residual 헬퍼 및 in-use 힌트, CLI stdout JSON/stderr 안내 분리, stop exit 4와 records preserved, 정상 purge 후 registry 행 제거 및 residual 출력 계약을 재확인했다.
|
||||
- CLAUDE_CONFIG_DIR 적용, codex/grok/opencode residual 테스트, stop SKILL/code-block 정합성 테스트 통과.
|
||||
- 실제 Mac 홈/tmp 구조의 read-only 메타데이터 확인은 동일 환경의 2차 리뷰 관측을 재사용했다. `/tmp`→`/private/tmp`, 별도 TMPDIR, per-uid workspace-key/UUID scratchpad 배치 및 ~/.claude 부속 디렉터리 구조는 그 보고서에 기록돼 있다. 이번에는 실제 세션 내용을 읽거나 실제 경로에 파괴적 검증을 추가하지 않았다.
|
||||
|
||||
브리프에서 수용한 비차단 제한은 유지된다: 링크된 projects/<key> 뒤 transcript는 삭제하지 않고 residual로 보고, lowercase 질의가 uppercase argv만 가진 보유자를 못 보는 경우, 직접 purge_artifacts 호출에는 in-use gate가 없는 점. 이를 이번 수정의 신규 차단 결함으로 재분류하지 않는다. 동시 파일시스템 경로 교체까지 원자적으로 방어함을 검증한 것은 아니다.
|
||||
|
||||
## 증거 파일
|
||||
|
||||
`/tmp/review-d5e47fc2-oqyptnum` (실경로 `/private/tmp/...`):
|
||||
|
||||
- `head.log`: 원본 HEAD 148 passed / 1 skipped.
|
||||
- `mutation.log`: 명령행 정규화 제거 대조군의 의도된 2건 실패.
|
||||
- `argv-probe.json`: lowercase UUID가 남은 BSD ps 결과, kill/wait 후 PID 부재.
|
||||
- `mutation/`: 원본과 분리된 회귀 대조군 복사본.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user