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,51 @@
|
||||
# 리뷰 리포트 — 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`, tree `f4d9422ad0c1d616b5f37065f3e96b34c28f78d8` — 일치. 트리 clean, `.mam` 없음(테스트 실행 후에도 동일).
|
||||
- 범위: base 26e2c43 .. head 6f4f40a (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/python3` 3.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` 는 그대로 둬도 된다.
|
||||
|
||||
## 확인한 사항 (정상)
|
||||
|
||||
1. **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`).
|
||||
2. **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>` 만 지워졌다.
|
||||
3. **실제 `~/.claude` 구조**(읽기만): `sessions/<pid>.json`(int `pid`, ms `startedAt`, `sessionId`) + `<pid>.<64hex>.key`, `jobs/<8hex>/state.json`(sessionId[:8] = 디렉터리명), `file-history/<uuid>/`, `session-env/<uuid>/`, `projects/<key>/<uuid>/{tool-results,…}` — 어댑터 가정과 일치.
|
||||
4. **삭제 범위**(샌드박스, `/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).
|
||||
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` 에서 안전.
|
||||
6. **residual 보고**: purge 후 `residual_artifacts` 가 빈 목록이면 `residual: none (agent: …)`. 실제 `~/.claude` 에 대한 이름 스캔(읽기 전용, 가짜 uuid)은 0.02 s.
|
||||
7. **Python 3.9 / bash 3.2**: 변경된 .py 8개 모두 `feature_version=(3,9)` 로 파싱되고, 프로브는 전부 `/usr/bin/python3` 3.9.6 으로 실행됐다(registry 가 모든 어댑터를 import). `lib.sh`·`stop_session.sh` 는 `/bin/bash` 3.2.57 `-n` 통과.
|
||||
8. **문서·skill_code_blocks.json**: stop SKILL.md 의 "purge 는 행을 지운다"·exit 4·삭제 목록이 코드와 맞고, `test_issue42_skill_blocks.py` 통과(sha·passthrough 라인 갱신 정합).
|
||||
9. **codex·grok·opencode residual**: 이름 스캔 + `mode=ro` SQLite 조회만 하므로 쓰기 없음.
|
||||
|
||||
## 테스트
|
||||
|
||||
- 검증 경로에서 `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개를 base 26e2c43 로 되돌리고 `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]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20261017T145452Z-6c5aa6",
|
||||
"base_sha": "26e2c43753169671b600d03274901dbe203988dd",
|
||||
"head_sha": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"tree_sha": "ffc291e6fc93822070f2b9f1d17009e83f176fcf",
|
||||
"review_commit": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"reviewer": "planner-reviewer-claude-01",
|
||||
"job_id": "48d0a01c",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-17T15:16:54Z"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# 리뷰 리포트 — PR #59 3차 (Issue #31) / job 48d0a01c
|
||||
|
||||
- 리뷰어: planner-reviewer-claude-01 (Opus 5.5)
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1`
|
||||
- identity: HEAD `de6f3fc81f8c33a9d2335320742dc5a47f566cda`, tree `ffc291e6fc93822070f2b9f1d17009e83f176fcf` 일치. 트리는 clean이고 `.mam` 없음(테스트 후에도 동일).
|
||||
- 범위: 26e2c43..de6f3fc 전체. 2a8d738 이후 변경은 de6f3fc 한 커밋으로, `tests/test_issue31_purge_residual.py` 의 +3/−1 뿐이다. `git diff 2a8d738 HEAD -- .agents` 는 비어 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
2차의 유일한 차단(T1: macOS `/bin/sh` 의 단일 명령 exec 로 보유자 argv 에서 uuid 가 사라지던 문제)이 해결됐다. 제품 코드는 2a8d738 과 바이트 단위로 같으므로, 2차에서 이 Mac 에서 확인한 내용이 그대로 유효하다. 확인한 항목은 비-UUID 거부, 링크 경계, 유한하지 않은 startedAt, UTC lstart, exit 4 안내, 삭제 범위, 실제 `~/.claude`·`/private/tmp/claude-501` 레이아웃 정합이다. 판정은 **PASS**.
|
||||
|
||||
## (a) de6f3fc 검증
|
||||
|
||||
1. **보유자 argv 에 uuid 가 남는다.**
|
||||
- 새 보유자는 `[sys.executable, "-c", "import time; time.sleep(120)", conv]` 이다(`tests/test_issue31_purge_residual.py:317`).
|
||||
- Python 인터프리터는 `-c` 뒤 인자를 `sys.argv[1]` 로 받을 뿐 exec 로 자신을 바꾸지 않으므로, `ps` 의 args 에 소문자 `conv` 가 그대로 남는다.
|
||||
- `subprocess.Popen` 은 자식의 exec 성공을 확인한 뒤에 반환하므로, 이어지는 `time.sleep(0.05)` 시점에는 argv 가 이미 보인다(경합 없음).
|
||||
- `sys` 는 파일 머리(`:22`)에서 이미 import 되어 있다. 같은 패턴인 `test_issue10_13_purge.py` 의 `_holder()` 와도 일관된다.
|
||||
2. **upper/mixed 에서 통과한다.** 검증 경로에서 `-k any_spelling` 을 5회 반복 실행해 5회 모두 `2 passed` 였다. 관련 모듈 전체 실행에서도 통과했다(아래).
|
||||
3. **철자 처리 회귀를 여전히 잡는다.** `git clone` 사본에서 claude 어댑터를 한 군데씩 변형해 test 파일 전체를 돌렸다.
|
||||
- `conversation_in_use` 의 argv 검색을 `{canon, uuid}` 에서 `{uuid}` 로 바꿈(소문자 정규화 제거): 2 failed (upper, mixed). 실패 지점은 정확히 새 보유자 단언 `:320`, `AssertionError: a command line naming the lowercase id` 이다. 즉 수정된 보유자가 이 회귀를 잡는 바로 그 장치다.
|
||||
- `_session_files(canon)` 을 `_session_files(uuid)` 로 바꿈: 2 failed (세션 파일 경로 단언에서).
|
||||
- `_canonical` 의 `.lower()` 제거: 2 failed.
|
||||
- 세 변형 모두 나머지 65개는 통과했으므로, 실패가 해당 테스트에 국한된다.
|
||||
4. **정리 동작이 유지된다.**
|
||||
- `try/finally: named.kill(); named.wait()` 구조는 그대로다(`:318-323`).
|
||||
- 변형 실행(일부러 실패시킨 경우 포함)이 끝난 뒤 `ps -axo pid=,args=` 에서 `python … import time; time.sleep(120)` 프로세스는 0개였다. argv0 가 python 인 행만 세어 자기 자신과의 매칭은 배제했다.
|
||||
|
||||
## (b) 전체 판정을 위한 재확인
|
||||
|
||||
- 제품 파일(`.agents/` 전체)은 2a8d738 과 동일하므로, 2차 리뷰(job 778fc866)의 확인 결과를 그대로 승계한다. 다시 돌린 것은 위 변형 대조와 아래 관련 모듈이다.
|
||||
- 알려진 비차단 세 가지는 브리프대로 병합 차단이 아니다. 내용은 2차 리포트에 적은 그대로다.
|
||||
- 링크된 `projects/<key>` 아래 jsonl 은 purge 되지 않고 residual 로만 보고된다.
|
||||
- 소문자 질의는 대문자 argv 보유자를 보지 못한다.
|
||||
- `purge_artifacts` 를 직접 호출하면 in-use 게이트를 거치지 않는다.
|
||||
|
||||
## 테스트
|
||||
|
||||
검증 경로에서 `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, test_tier1_unit, test_tier2_component`
|
||||
- 결과: **332 passed, 1 skipped, 1 error** (624 s). 2차의 2 failed 는 사라졌다.
|
||||
- 1 error 는 `test_tier2_component.py::test_comp_codex_create_and_reconcile_and_stop` 의 **teardown 에서 #55 가드**가 낸 것으로, 브리프가 밝힌 알려진 오탐이다. 판정 근거로 쓰지 않고 보고만 한다.
|
||||
- 가드가 잡은 경로: `~/.gemini/antigravity-cli/conversations/0dd4a87f-6170-472b-886a-eef6e099b375.db-shm` 과 `.db-wal`.
|
||||
- 읽기 전용 확인 결과: 이 DB 는 다른 곳에서 돌고 있는 실제 `agy` 프로세스(pid 66733)가 열고 있다(`lsof -t`). WAL 이 자라는 중이다(mtime Oct 18 00:06, 크기 19.6 MB). 테스트 산출물이 아니다.
|
||||
- 이 테스트는 2차 실행에서는 통과했고, 이번 커밋은 tier2 와 무관한 테스트 한 줄만 바꿨다.
|
||||
|
||||
## 비차단 메모
|
||||
|
||||
- 2차 리포트의 비차단 사항(`projects/<key>` 링크 시 동작 문서화 등)은 그대로다. 병합 후 별도 이슈로 다뤄도 된다.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,95 @@
|
||||
# 리뷰 리포트 — PR #59 2차 (Issue #31) / job 778fc866
|
||||
|
||||
- 리뷰어: planner-reviewer-claude-01 (Opus 5.5)
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1`
|
||||
- identity: HEAD `2a8d73825b3832376aeaf96b09548359dc3cb887`, tree `d146f1ac68bcc3485d24fb92edb87264e4d1e8cb` 일치. 트리는 clean이고 `.mam` 없음(테스트 후에도 동일).
|
||||
- 범위: 26e2c43..2a8d738 누적, 중점은 1차 이후 커밋 11개(26fb1c4..2a8d738)
|
||||
|
||||
## 요약
|
||||
|
||||
1차 수정 요청 3건(차단 F1, P1, P2)은 **제품 코드에서 모두 해결됐다**. 비차단 메모(서머타임/UTC, exit 4 안내, SKILL.md 문구, 테스트의 실제 `/tmp` 나열)도 해결됐다. 샌드박스 재현과 변형(mutation) 대조로 각 수정과 회귀 테스트가 실제로 맞물리는 것도 확인했다.
|
||||
|
||||
그러나 **새로 추가된 테스트 `test_any_spelling_of_the_uuid_is_the_lowercase_session[upper|mixed]` 는 이 Mac 에서 항상 실패한다**(T1). 원인은 제품 코드가 아니라 테스트의 macOS 이식성 버그다. 이대로 병합하면 이 Mac 의 스위트가 빨간불이 되므로 NOT PASS 로 판정한다. 수정은 테스트 한 줄이다.
|
||||
|
||||
## 차단 결함
|
||||
|
||||
### T1. `sh -c "sleep 120" <uuid>` 보유자 프로세스가 macOS 에서 uuid 를 argv 에서 잃는다
|
||||
|
||||
- 위치: `tests/test_issue31_purge_residual.py:315-318`
|
||||
```python
|
||||
named = subprocess.Popen(["sh", "-c", "sleep 120", conv])
|
||||
time.sleep(0.05)
|
||||
assert adapter.conversation_in_use(other, ctx) is True, "a command line naming the lowercase id"
|
||||
```
|
||||
- 원인: 이 Mac 의 `/bin/sh` 는 GNU bash 3.2.57 이다. bash 는 `-c` 의 단일 단순 명령을 exec 로 바꿔 실행하므로, 실행된 프로세스의 argv 는 `sleep 120` 만 남고 `$0` 인 uuid 는 사라진다.
|
||||
- 직접 확인: `/bin/sh -c 'sleep 30' MARKER-abc &` → `ps` 에 `sleep 30` 만 보인다.
|
||||
- `/bin/sh -c 'sleep 30; :' MARKER-abc` → `/bin/sh -c sleep 30; : MARKER-abc` 로 uuid 가 남는다.
|
||||
- 이 저장소도 같은 함정을 이미 기록해 두었다: `tests/test_issue10_13_purge.py:114` ("a shell execs its last simple command and the uuid …").
|
||||
- 재현: 검증 경로의 관련 모듈 실행(아래)에서 **2 failed**. 수정하지 않은 `git clone` 사본에서 `-k any_spelling` 만 돌려도 2 failed:
|
||||
`AssertionError: a command line naming the lowercase id … conversation_in_use('91151A0D-0114-4C3C-a120-aec043818d1f', …) → False`
|
||||
아래 변형 대조 4종(F1/P1/P2/UTC)에서도 변형과 무관하게 같은 2개가 실패했다.
|
||||
- 제품 로직은 정상이다. argv 를 유지하는 보유자(`["sh","-c","sleep 30; :", <lowercase uuid>]`)로 샌드박스에서 확인했다:
|
||||
- `conversation_in_use(UPPER)` = True, `conversation_in_use(lower)` = True.
|
||||
- 단일 명령 보유자로는 두 철자 모두 False.
|
||||
- 제안: 보유자를 `["sh", "-c", "sleep 120; :", conv]` 나 `[sys.executable, "-c", "import time; time.sleep(120)", conv]` 처럼 uuid 가 argv 에 남는 형태로 바꾼다. 고정 `time.sleep(0.05)` 대신 짧은 폴링(예: 최대 2 s 동안 `ps` 에 보일 때까지)으로 기다리면 느린 호스트의 경합도 피할 수 있다.
|
||||
|
||||
## 1차 지적사항 확인
|
||||
|
||||
샌드박스 조건: HOME·CLAUDE_PROJECT_DIR·CLAUDE_CODE_TMPDIR·TMPDIR 를 모두 스크래치패드 아래로 두고, CLAUDE_CONFIG_DIR 은 비웠다. 인터프리터는 `/usr/bin/python3` 3.9.6. 실제 경로는 읽기만 했다.
|
||||
|
||||
1. **[차단] 비-UUID id — 해결.**
|
||||
- 방식: `_canonical()`(`claude.py:8-19`)이 정규 UUID 만 받아 소문자로 바꾼다. `_candidates`·`purge_artifacts`·`residual_artifacts`·`conversation_in_use` 의 모든 입구가 이 함수를 거친다.
|
||||
- 재현: 아래 14개 id 는 purge 결과가 모두 rc 5 이고, 아무것도 지우지 않았다. `projects/<key>/memory/MEMORY.md` 와 다른 세션 파일은 전부 남았다.
|
||||
- `""`, `.`, `..`, `a/b`, `*`, `../x`
|
||||
- `memory`, `MEMORY`, `subagents`, `<key>`
|
||||
- uuid 앞 8자리, `<U>x`, `" <U>"`, `"<U>\n"`
|
||||
- 대문자 uuid 로 purge 하면 소문자 경로 9개가 정확히 지워진다(`lower jsonl gone True`). 다른 세션·공유 파일은 모두 보존됐다.
|
||||
- 변형 대조(`_canonical` 을 옛 `_plausible_id` 규칙으로 되돌림): `rejects_malformed_uuid[memory, ' ', notes, …g, '{conv} ', 'x{conv}']`, `a_non_uuid_id_reaches_nothing[uuid-c, memory, notes]`, `any_spelling` 이 실패해 **11 failed**.
|
||||
2. **[P1] 링크 경계 — 해결.**
|
||||
- 방식: `_inside()`(`claude.py:109-123`)는 루트와 대상 사이의 디렉터리 구성요소를 `lstat` 으로 하나씩 확인하고, 부모의 realpath 가 루트의 realpath 아래에 있는지 본다.
|
||||
- 링크로 연결된 tmp `<key>`, `file-history/`, `projects/<key>` 뒤의 내용은 모두 보존되고 `residual` 로 보고된다.
|
||||
- 말단 링크(`session-env/<U> → 외부`)는 링크만 지워지고 대상 파일은 남는다.
|
||||
- 루트 자체가 링크인 경우(`~/.claude → realhome`, `CLAUDE_CODE_TMPDIR → realtmp`, 즉 `/tmp → /private/tmp` 같은 형태)는 정상적으로 purge 된다.
|
||||
- Python 3.9 macOS 의 `shutil._use_fd_functions` 는 True 로, rmtree 가 내부 링크를 따라가지 않는다.
|
||||
- 변형 대조(`_inside` → 항상 True): `never_follows_a_linked_dir_below_a_root`, `reports_a_linked_orphan_it_will_not_remove` 실패(T1 의 2개 포함 **4 failed**).
|
||||
3. **[P2] 유한하지 않은 startedAt — 해결.**
|
||||
- 방식: `claude.py:84-96`.
|
||||
- 살아 있는 `sleep` 보유자에 대해 startedAt 을 아래 9가지 값으로 바꿔 봤다. 모두 in-use rc 4, purge rc 4 였고 jsonl 은 보존됐다.
|
||||
- `NaN`, `Infinity`, `-Infinity`, `"NaN"`, `true`, `1e400`, 400자리 정수, `null`, `"1760000000000"`
|
||||
- 정상 ms 값이면 live(rc 4), 1시간 전 값(pid 재사용)이면 free 다.
|
||||
- 변형 대조(`isfinite` 제거): `unusable_started_at_means_live[nan]`, `live_holder…[NaN]` 실패. inf 계열은 다른 분기에서도 live 로 판정되므로 이 변형으로는 실패하지 않는 것이 맞다.
|
||||
|
||||
## 비차단 메모 확인
|
||||
|
||||
- **UTC (af8ed39)**: 실제 살아 있는 claude 4개를 읽기 전용으로 비교했다(`TZ=UTC0 LC_ALL=C ps -o lstart=` + `calendar.timegm`).
|
||||
- lstart 가 세션 파일의 `procStart`(UTC)와 초 단위까지 일치했다. 예: `Sat Oct 17 13:55:10 2026`.
|
||||
- startedAt − lstart 는 1.34 / 1.49 / 1.99 / 2.36 s 로, 모두 live 판정이다.
|
||||
- 변형 대조(mktime·로컬 TZ 로 되돌림): `liveness_reads_lstart_as_utc` 실패.
|
||||
- **exit 4 안내 (e156179)**: in-use stderr 가 두 가지를 출력한다.
|
||||
- "a claude process whose session file under <cfg>/sessions/ names it" (CLAUDE_CONFIG_DIR 을 반영)
|
||||
- "ps -axww | grep <uuid>"
|
||||
- `stop_session.sh:292` 의 `h="$(… 2>&1 >/dev/null)" || rc=$?` 는 `/bin/bash` 3.2 + `set -euo pipefail` 에서 rc 4 와 안내 문구를 올바르게 잡고, 안내가 비었을 때도 스크립트가 계속 진행된다(직접 확인). `bash -n` 통과.
|
||||
- **SKILL.md exit 4 문구**: "Records preserved … the session itself may already be down" 으로 정확해졌다.
|
||||
- **테스트의 실제 /tmp (26fb1c4)**: autouse 픽스처가 `_SYSTEM_TMP` 를 테스트 디렉터리로 패치하고, `test_tests_never_reach_the_real_tmp_root` 가 하위 프로세스까지 확인한다.
|
||||
- **agy 게이트**: 동작 변화는 그대로다(fail-closed). 실제 agy 로는 확인하지 않았다(브리프상 금지).
|
||||
|
||||
## 회귀 관점 (dd292c1 등)
|
||||
|
||||
- `has_artifacts` 는 링크 뒤의 후보도 포함하므로 `purge auto` 가 claude 를 고른다. 이때 purge 는 아무것도 지우지 않고 residual 만 보고한다(위 links 재현: rc 0, `purged` 에는 말단 링크 1개만 있고 `residual` 4개). 의도대로다.
|
||||
- 동작이 바뀌는 지점이 하나 있다(비차단): `projects/<key>` 자체가 심볼릭 링크인 사용자는 이제 transcript jsonl 이 purge 되지 않고 residual 로만 보고된다. 반면 `discover`/`verify_artifact` 는 링크를 따라가 그 대화를 계속 찾는다. P1 요구("링크를 따라가지 않음")의 결과이고 residual 로 드러나므로 수용 가능하지만, SKILL.md 에 한 줄 적어 두면 좋다.
|
||||
- 철자 비대칭(비차단): 소문자 id 로 질의하면 대문자 argv 보유자는 보지 못한다(`claude -r <UPPER>` 로 띄운 경우). claude 가 소문자로 기록하는 한 실질 영향은 없다.
|
||||
- `base.in_use_hints` 추가와 `__main__` 의 in-use stderr 출력은 다른 어댑터에 기본 안내만 준다. 회귀 없음.
|
||||
- `test_a4_adapter_contract` 의 claude id 를 실제 uuid 로 바꾼 것은 새 계약(정규 UUID 만 허용)에 따른 정당한 갱신이다.
|
||||
- Python 3.9 파싱(`feature_version=(3,9)`)과 `/usr/bin/python3` 실행(모든 프로브)에서 문제 없었다.
|
||||
|
||||
## 테스트
|
||||
|
||||
- 검증 경로, `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, test_tier1_unit, test_tier2_component`
|
||||
- 결과: **2 failed (T1), 330 passed, 1 skipped** (630 s).
|
||||
- 사본에서 `test_issue31_purge_residual.py` 를 반복 실행한 결과:
|
||||
- 비변형 사본: 2 failed (T1) / 65 passed.
|
||||
- P2 변형 사본: 첫 실행에서 `test_grok_residual_reports_memory_observations` 가 한 번 ERROR 였다. 이후 재실행 4회에서는 재현되지 않아 일회성으로 보고만 한다.
|
||||
- 6f4f40a 의 제품 파일로 통째로 되돌린 대조는 쓸 수 없었다. autouse 픽스처가 26fb1c4 에서 추가된 `claude._SYSTEM_TMP` 를 패치하므로 67개 모두 setup ERROR 가 난다. 그래서 위의 수정별 변형 대조로 대신했다.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -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]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Review Report — PR #59 (Issue #31: Complete Claude purge, residual reporting, and live-process stop gate)
|
||||
|
||||
- **Reviewer**: `agy` (`herdr:reviewer-agy-01`)
|
||||
- **Job ID**: `2fff60bd`
|
||||
- **Branch / Range**: `fix/issue-31` (`26e2c43753169671b600d03274901dbe203988dd..6f4f40a4adb2115d5997dffd31fb6473f05cab68`, 13 commits, 12 files)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1`
|
||||
- **Platform**: macOS Darwin (`arm64-apple-darwin25`, `/usr/bin/python3` 3.9.6, `.venv/bin/python` 3.9.6, `/bin/bash` 3.2.57)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
PR #59 resolves Issue #31 by unifying the Claude adapter's owned-path resolution (`_owned_paths`) across `purge_artifacts`, `has_artifacts`, and `residual_artifacts`, covering:
|
||||
- `projects/<key>/<uuid>.jsonl` and `projects/<key>/<uuid>/` (across both `ctx.claude_dir` and `<config_dir>/projects`),
|
||||
- `file-history/<uuid>/` and `session-env/<uuid>/`,
|
||||
- `jobs/<uuid[:8]>/` (only when `state.json` has `sessionId == uuid`, removing `state.json` last so partial failure preserves ownership evidence),
|
||||
- `sessions/<pid>.<64hex>.key` (masked in output as `<pid>.*.key`) and `sessions/<pid>.json` for dead or PID-reused processes (removing keys before the `.json` file so ownership evidence survives partial failure),
|
||||
- `<tmp>/claude-<uid>/<key>/<uuid>/` across `CLAUDE_CODE_TMPDIR`, `TMPDIR`/`TMP`/`TEMP`, and `/tmp`.
|
||||
|
||||
It also adds `residual_artifacts` for `claude`, `codex`, `grok`, and `opencode`; wires `stop_session.sh` to report residuals after `--purge-conversation` and to check `python -m lib_py.agents in-use <agent> <uuid> <cwd> --wait 5` before purging (exiting 4 without mutating state if a live agent process still holds the conversation); supports `CLAUDE_CONFIG_DIR` across `lib.sh`, `DiscoveryContext`, `claude.py`, and `_agent_home_guard.py`; and updates `multi-agent-mux-stop/SKILL.md` + `tests/data/skill_code_blocks.json` to reflect that `--purge-conversation` removes the session row rather than leaving `status: terminated`.
|
||||
|
||||
All 12 modified files pass Python 3.9.6 AST compilation and Bash 3.2 (`set -u`) syntax checks, all 11 regression test cases fail against base `26e2c43` and pass on head `6f4f40a`, and 217 tests across 6 test suites pass on macOS.
|
||||
|
||||
---
|
||||
|
||||
## 2. macOS Environment & Deletion-Scope Safety Verification
|
||||
|
||||
1. **BSD `ps -o lstart=` Format, Locale (`LC_ALL=C`), and `startedAt + 2` Comparison on macOS**:
|
||||
- Verified BSD `ps -p <pid> -o lstart=` on this Mac under `LC_ALL=C`: outputs `'Fri Oct 16 09:48:41 2026 \n'`.
|
||||
- `" ".join(out.split())` normalizes multi-space padding (including single-digit days like `'Oct 2'`) and trailing whitespace before `time.strptime(..., "%a %b %d %H:%M:%S %Y")`.
|
||||
- Inspected 4 live `~/.claude/sessions/<pid>.json` files on this Mac (read-only): `startedAt / 1000.0 - lstart` ranged from `+0.694 s` to `+2.430 s`, so `lstart <= started + 2` holds for all live processes and rejects reused PIDs started after the session file was written.
|
||||
- Fail-safe behavior: invalid/non-positive PIDs, `started <= 0`, missing `startedAt`, or `ps`/parse errors all return `True` (treated as live, refusing purge).
|
||||
|
||||
2. **macOS Temp Root Resolution (`TMPDIR` vs `/tmp` -> `/private/tmp`) & Real Layout Agreement**:
|
||||
- On this Mac, `TMPDIR` is `/var/folders/q_/.../T/` (which has no `claude-501`), while Claude Code's actual scratchpad directory is `/private/tmp/claude-501/<workspace_key>/<uuid>/` (accessed via `/tmp/claude-501` symlink).
|
||||
- `_tmp_roots()` checks `CLAUDE_CODE_TMPDIR` (both `<override>/claude-<uid>` and `<override>` when already named `claude-<uid>`), `TMPDIR`/`TMP`/`TEMP`, and `/tmp/claude-<uid>`, deduplicating via `os.path.realpath(c)`. This ensures `/private/tmp/claude-<uid>` is always discovered on macOS even when `TMPDIR` points to `/var/folders/.../T/`.
|
||||
- Read-only inspection of `~/.claude` on this Mac confirmed exact structural agreement for `projects/<key>/`, `file-history/<uuid>/`, `session-env/<uuid>/`, `sessions/<pid>.json` + `<pid>.<64hex>.key`, and `jobs/<uuid[:8]>/state.json` (while shared files like `jobs/pins.json` and `history.jsonl` are untouched).
|
||||
|
||||
3. **Deletion Scope Isolation, Malformed UUID Rejection, and Symlink Safety**:
|
||||
- `_plausible_id(uuid)` rejects `""`, `"."`, `".."`, `"*"`, and any string containing `"/"`, `".."`, or `"*"` before constructing any path or scanning any directory.
|
||||
- `_owned_job` checks `json.load(open(job/state.json)).get("sessionId") == uuid`, leaving another session's `jobs/<uuid[:8]>/` (and `jobs/pins.json`) untouched.
|
||||
- `_key_files` matches `rf"{stem}\.[0-9a-f]{{64}}\.key"` only for a `sessions/<pid>.json` whose `sessionId == uuid`, never reading key contents and never touching lone keys or other sessions' PIDs.
|
||||
- `_remove` checks `if os.path.islink(path) or not os.path.isdir(path): os.remove(path)` before `shutil.rmtree(path, ignore_errors=True)`, and `scan_names` uses `os.walk(root)` (`followlinks=False`), ensuring symlinks are never traversed outside the target scope.
|
||||
|
||||
4. **Stop Exit 4 Contract, Residual Reporting, and Documentation Lockstep**:
|
||||
- `stop_session.sh` runs `"$(_delegate_py_bin)" -m lib_py.agents in-use "$AGENT" "$PURGE_UUID" "$TARGET_CWD" --wait 5` before `atomic_dump_yaml`; if non-zero, it publishes a `cancelled` job event and exits `4` with all records and files preserved.
|
||||
- `tests/data/skill_code_blocks.json` hashes (`301c347ff881`, `a973676cb2d1`) and line number (`stop_session.sh:327`) match `multi-agent-mux-stop/SKILL.md` and `stop_session.sh` (`test_issue42_skill_blocks.py` passes).
|
||||
|
||||
---
|
||||
|
||||
## 3. Test Suite Execution Summary
|
||||
|
||||
Executed in `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1` with `HERDR_*`, `WORKSPACE_ROOT`, `MAM_ENV_FILE`, `MAM_JOBS_DIR`, `DELEGATE_JOB_LOGS_DIR`, and `CLAUDE_CONFIG_DIR` scrubbed, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp=/tmp/mam_verify_pr59_bt`:
|
||||
|
||||
- `tests/test_issue31_purge_residual.py`: **32 passed** (11 core regression cases verified to fail on base `26e2c43`)
|
||||
- `tests/test_issue55_agent_homes.py`: **11 passed**
|
||||
- `tests/test_issue42_skill_blocks.py`: **15 passed**
|
||||
- `tests/test_a4_adapter_contract.py`: **55 passed**
|
||||
- `tests/test_orc_onboard.py`: **37 passed, 1 skipped**
|
||||
- `tests/test_tier1_unit.py`: **67 passed**
|
||||
|
||||
Total: **217 passed, 1 skipped, 0 failed**. Verification worktree `git status -s` is empty and no `.mam` directory was created.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20261017T145452Z-6c5aa6",
|
||||
"base_sha": "26e2c43753169671b600d03274901dbe203988dd",
|
||||
"head_sha": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"tree_sha": "ffc291e6fc93822070f2b9f1d17009e83f176fcf",
|
||||
"review_commit": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
|
||||
"reviewer": "reviewer-agy-01",
|
||||
"job_id": "7c325532",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-17T15:16:54Z"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Pre-Merge Review Report (Round 3): PR #59 (`fix/issue-31`)
|
||||
|
||||
- **Job ID**: `7c325532`
|
||||
- **Reviewer**: `herdr:reviewer-agy-01` (`agy`)
|
||||
- **Commit Range**: `26e2c43753169671b600d03274901dbe203988dd` .. `de6f3fc81f8c33a9d2335320742dc5a47f566cda` (25 commits; Round 3 delta: `2a8d73825b3832376aeaf96b09548359dc3cb887..de6f3fc81f8c33a9d2335320742dc5a47f566cda`, 1 commit)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1`
|
||||
- **Host / Runtime**: macOS `arm64-apple-darwin25` (Darwin 25.3.0), `/usr/bin/python3` 3.9.6 & `.venv/bin/python` 3.9.6, `/bin/bash` 3.2.57
|
||||
|
||||
---
|
||||
|
||||
## 1. Round 3 Delta Verification (`2a8d738..de6f3fc`)
|
||||
|
||||
- **Commit `de6f3fc81f8c33a9d2335320742dc5a47f566cda`**:
|
||||
- Replaces `subprocess.Popen(["sh", "-c", "sleep 120", conv])` at `tests/test_issue31_purge_residual.py:315` with:
|
||||
```python
|
||||
# Not `sh -c "sleep 120" <uuid>`: a shell execs its last simple command
|
||||
# and the uuid would vanish from the process list (macOS /bin/sh).
|
||||
named = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])
|
||||
```
|
||||
- **Verification Checklist**:
|
||||
1. **Lowercase UUID retained in `argv` on macOS**: The Python holder process does not `exec` a separate binary, so `conv` stays visible in `ps -axo pid=,command=` for the lifetime of `named`.
|
||||
2. **Passes `[upper]` and `[mixed]` on macOS**: Both `test_any_spelling_of_the_uuid_is_the_lowercase_session[upper]` and `[mixed]` pass on Darwin (`67/67` tests in `tests/test_issue31_purge_residual.py` pass).
|
||||
3. **Regression sensitivity preserved**: Reverting `for u in {canon, uuid}` to `for u in (uuid,)` in `ClaudeAgentAdapter.conversation_in_use` (in an isolated `/tmp` copy) causes both `[upper]` and `[mixed]` to fail immediately at `tests/test_issue31_purge_residual.py:320` (`AssertionError: a command line naming the lowercase id`).
|
||||
4. **Process cleanup (`finally: named.kill(); named.wait()`)**: Because `named` is a single Python process with no forked child `sleep`, `named.kill()` and `named.wait()` terminate it cleanly without leaving orphaned processes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Cumulative Review Summary (`26e2c43..de6f3fc`)
|
||||
|
||||
- **Canonical UUID & deletion scope safety**: `_canonical(uuid)` in `.agents/skills/lib_py/agents/adapters/claude.py` gates `_candidates`, `has_artifacts`, `conversation_in_use`, `purge_artifacts`, and `residual_artifacts`, preventing non-UUID names (`memory`, `notes`, `uuid-c`, etc.) from matching or deleting `projects/<key>/memory/` or any other directory.
|
||||
- **Symlink boundary protection**: `_inside(root, path)` verifies via `os.lstat` that no intermediate directory component between `root` and `os.path.dirname(path)` is a symlink and that `realpath(dirname(path))` stays strictly inside `realpath(root)`, while `has_artifacts` counts `_candidates` so symlinked orphans still select `claude` and appear in `residual_artifacts`.
|
||||
- **macOS `ps -o lstart=` & `startedAt` fail-safe liveness**: `_session_file_live` runs `ps -p <pid> -o lstart=` with `LC_ALL="C", TZ="UTC0"`, parses via `calendar.timegm`, and treats `bool`, non-`(int, float)`, `OverflowError`, `NaN`, `±inf`, and `<= 0` `startedAt` values as live when the PID exists.
|
||||
- **`stop_session.sh` & CLI `in-use` gate**: `in_use_hints` and exit-4 error messages accurately report where live holders may be found and that registry records and conversation files are preserved.
|
||||
- **Test suite execution**:
|
||||
- `tests/test_issue31_purge_residual.py`: **67 passed**
|
||||
- `tests/test_issue55_agent_homes.py`: **PASSED**
|
||||
- `tests/test_issue42_skill_blocks.py`: **PASSED**
|
||||
- `tests/test_a4_adapter_contract.py`: **PASSED**
|
||||
- `tests/test_orc_onboard.py`: **PASSED**
|
||||
- Total run in Round 3 verification worktree: **185 passed, 1 skipped** in 48.05s (`test_tier1_unit.py` also verified passing in Round 2 with no production changes since `2a8d738`).
|
||||
- **Worktree hygiene**: Verification worktree `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1` is clean (`git status -s` empty, no `.mam` directory created, basetemp removed).
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,81 @@
|
||||
# Pre-Merge Review Report (Round 2): PR #59 (`fix/issue-31`)
|
||||
|
||||
- **Job ID**: `b4aa3aff`
|
||||
- **Reviewer**: `herdr:reviewer-agy-01` (`agy`)
|
||||
- **Commit Range**: `26e2c43753169671b600d03274901dbe2039887` .. `2a8d73825b3832376aeaf96b09548359dc3cb887` (Round 2 delta: `6f4f40a4adb2115d5997dffd31fb6473f05cab68..2a8d73825b3832376aeaf96b09548359dc3cb887`, 11 commits)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1`
|
||||
- **Host / Runtime**: macOS `arm64-apple-darwin25` (Darwin 25.3.0), `/usr/bin/python3` 3.9.6 & `.venv/bin/python` 3.9.6, `/bin/bash` 3.2.57
|
||||
|
||||
---
|
||||
|
||||
## 1. Round 1 Findings Verification
|
||||
|
||||
All three required production fixes and all four non-blocking improvements from Round 1 are cleanly resolved in `6f4f40a..2a8d738`:
|
||||
|
||||
1. **[Blocker — Resolved] Canonical UUID enforcement across Claude adapter (`bd09c6a`, `c0248b8`, `81b6c2e`, `2a8d738`)**:
|
||||
- `_canonical(uuid)` in `.agents/skills/lib_py/agents/adapters/claude.py:10-18` validates `isinstance(uuid, str) and _UUID.fullmatch(uuid)` (`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`, case-insensitive) and normalizes to lowercase.
|
||||
- `_candidates`, `has_artifacts`, `conversation_in_use`, `purge_artifacts`, and `residual_artifacts` all gate on `_canonical(uuid)`, ensuring non-UUID names (such as `memory`, `notes`, or `uuid-c`) never touch `projects/<key>/memory/` or any other directory/file.
|
||||
- `conversation_in_use` checks `any(conversation_in_use(u, [self.artifact_path(canon, ctx)]) for u in {canon, uuid})` plus `self._session_files(canon, ctx)`, properly handling uppercase/mixed-case UUIDs on case-insensitive APFS.
|
||||
- `tests/test_a4_adapter_contract.py:225-232` was updated with user approval to use a canonical UUID (`0c1a5f3e-6b2d-4c8e-9a7f-1d2e3f4a5b6c`).
|
||||
|
||||
2. **[P1 — Resolved] Symlink boundary check below Claude roots (`6e0361c`, `dd292c1`)**:
|
||||
- `_inside(root, path)` in `.agents/skills/lib_py/agents/adapters/claude.py:108-122` walks every path component of `os.path.relpath(os.path.dirname(path), root)` and verifies `stat.S_ISDIR(os.lstat(cur).st_mode)` (rejecting any intermediate symlink) and `os.path.realpath(os.path.dirname(path)).startswith(real_root.rstrip(os.sep) + os.sep)`.
|
||||
- Roots that are themselves symlinks (such as `/tmp -> /private/tmp` on macOS or a symlinked `~/.claude`) and leaf symlinks (`_remove` unlinks the leaf symlink without traversing) work as intended.
|
||||
- `has_artifacts` checks `bool(self._candidates(uuid, ctx))` so a candidate behind a symlink still selects `claude` in `purge auto` / `purge claude` and is reported in `residual_artifacts` without being deleted.
|
||||
|
||||
3. **[P2 — Resolved] Fail-safe `startedAt` validation in `_session_file_live` (`f943b0d`, `60779ba`)**:
|
||||
- `.agents/skills/lib_py/agents/adapters/claude.py:87-95` rejects `bool`, non-`(int, float)`, `OverflowError` on `float(started) / 1000.0` (e.g. `10**400`), `NaN`, `±inf`, and `<= 0`, returning `True` (fail-safe live) whenever the PID is alive.
|
||||
|
||||
4. **[Non-Blocking Notes — Resolved]**:
|
||||
- **UTC `lstart` parsing (`af8ed39`)**: `ps -p <pid> -o lstart=` runs with `LC_ALL="C", TZ="UTC0"` and parses via `calendar.timegm`. Verified directly on macOS Darwin with both `.venv/bin/python` and `/usr/bin/python3` (`abs(time.time() - lstart) < 0.7s`).
|
||||
- **Actionable `in_use_hints` & exit-4 wording (`e156179`)**: `BaseAgentAdapter.in_use_hints` and `ClaudeAgentAdapter.in_use_hints` print the active `<cfg>/sessions/` path and `ps -axww | grep <uuid>` hint on stderr when `in-use` or `stop_session.sh --purge-conversation` exits 4; `SKILL.md` and `stop_session.sh` accurately state that registry records and conversation files are preserved even though the herdr session itself may already be down.
|
||||
- **Host `/tmp/claude-<uid>` test isolation (`26fb1c4`)**: `_SYSTEM_TMP = "/tmp"` in `claude.py` is redirected by the `own_tmp_roots` autouse fixture (and `sitecustomize.py` for child Python processes) in `tests/test_issue31_purge_residual.py`.
|
||||
|
||||
5. **Pre-fix failure verification**:
|
||||
- Running the new Round 2 regression tests from `tests/test_issue31_purge_residual.py` against pre-fix code (`26fb1c4`, before `bd09c6a..2a8d738`) in an isolated `/tmp` copy produced **25 test failures** across all targeted bug classes (`test_claude_rejects_malformed_uuid`, `test_any_spelling_of_the_uuid_is_the_lowercase_session`, `test_claude_purge_never_follows_a_linked_dir_below_a_root`, `test_purge_cli_reports_a_linked_orphan_it_will_not_remove`, `test_a_non_uuid_id_reaches_nothing`, `test_liveness_reads_lstart_as_utc`, `test_an_unusable_started_at_means_live`, `test_a_live_holder_with_an_unusable_started_at_blocks_the_purge`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Remaining Issue Blocking Merge
|
||||
|
||||
### [P1] `test_any_spelling_of_the_uuid_is_the_lowercase_session` fails on macOS due to `/bin/sh -c` single-command `exec` optimization
|
||||
|
||||
- **Location**: `tests/test_issue31_purge_residual.py:315` (introduced in commit `c0248b8`)
|
||||
- **Symptom**:
|
||||
Running pytest on macOS fails both parametrizations (`[upper]` and `[mixed]`):
|
||||
```text
|
||||
FAILED tests/test_issue31_purge_residual.py::test_any_spelling_of_the_uuid_is_the_lowercase_session[upper]
|
||||
FAILED tests/test_issue31_purge_residual.py::test_any_spelling_of_the_uuid_is_the_lowercase_session[mixed]
|
||||
AssertionError: a command line naming the lowercase id
|
||||
assert False is True
|
||||
```
|
||||
- **Root Cause**:
|
||||
Line 315 spawns the dummy holder process as:
|
||||
```python
|
||||
named = subprocess.Popen(["sh", "-c", "sleep 120", conv])
|
||||
```
|
||||
On macOS (`/bin/sh` is GNU bash 3.2.57 in POSIX mode), `sh -c "sleep 120" <conv>` sees a single simple command with no subsequent commands or traps and optimizes by calling `execve("/bin/sleep", ["sleep", "120"])` directly in-place (`CMD_NO_FORK`). Within ~10ms (before `time.sleep(0.05)` finishes), the process image and argv in `ps -axo pid=,command=` become `sleep 120`, and `$0` (`conv`) vanishes from the process list.
|
||||
Note that `tests/test_issue10_13_purge.py:111-118` (`_holder`) already documents this exact pitfall:
|
||||
> `(Not sh -c 'sleep …': a shell execs its last simple command and the uuid would vanish from the process list.)`
|
||||
- **Required Fix**:
|
||||
In `tests/test_issue31_purge_residual.py:315`, use `sys.executable` instead of `sh -c`:
|
||||
```python
|
||||
named = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])
|
||||
```
|
||||
*(Verified in an isolated `/tmp` copy on macOS: replacing line 315 with `sys.executable` makes both `[upper]` and `[mixed]` pass in 2.24s, with no orphaned child `sleep` process after `named.kill()`).*
|
||||
|
||||
---
|
||||
|
||||
## 3. Verification Summary
|
||||
|
||||
- Targeted pytest suite run against `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1` with scrubbed herdr/MAM/Claude environment variables:
|
||||
- `tests/test_issue55_agent_homes.py`: **PASSED**
|
||||
- `tests/test_issue42_skill_blocks.py`: **PASSED**
|
||||
- `tests/test_a4_adapter_contract.py`: **PASSED**
|
||||
- `tests/test_orc_onboard.py`: **PASSED**
|
||||
- `tests/test_tier1_unit.py`: **PASSED**
|
||||
- `tests/test_issue31_purge_residual.py`: **65 passed, 2 failed** (`test_any_spelling_of_the_uuid_is_the_lowercase_session[upper]` and `[mixed]`)
|
||||
- Total: **250 passed, 2 failed, 1 skipped** (all 252 pass once `tests/test_issue31_purge_residual.py:315` uses `sys.executable`).
|
||||
- Verification worktree `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1` verified clean (`git status -s` empty, no `.mam` directory created).
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Review 916abd41 — PR #59 second pass, Issue #31
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1` (`loop-20261017T135731Z-e93f0a`). Head `2a8d73825b3832376aeaf96b09548359dc3cb887`, tree `d146f1ac68bcc3485d24fb92edb87264e4d1e8cb`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Versus base: 13 files, +1462/−30. Versus the first-review head `6f4f40a4adb2115d5997dffd31fb6473f05cab68`: 11 commits (`26fb1c4`..`2a8d738`), 7 files, +382/−50. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
|
||||
|
||||
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57 (and `/bin/sh` is that bash). `bash -n` accepts `stop_session.sh` and `lib.sh`.
|
||||
|
||||
Every purge and `in-use` exercised here used a temporary `HOME`, `CLAUDE_CONFIG_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_TMPDIR`, and `TMPDIR`. The real `~/.claude` and `/private/tmp/claude-<uid>` were listed with read-only counts. `_session_file_live` was called on the real session files and deleted nothing.
|
||||
|
||||
## What this Mac already stores
|
||||
|
||||
`~/.claude` has real directories `file-history`, `session-env`, `sessions`, `jobs`, and `projects`. `file-history` holds 19 entries and no symlinks. `session-env` holds 21 entries and no symlinks. `sessions` holds 6 `*.json` files and 6 key files. `jobs` holds 3 directories, none of them symlinks, and 2 of those names are 8 characters. `projects` has 16 keys, none of them symlinks.
|
||||
|
||||
`/tmp` is a symlink to `/private/tmp`. `/tmp/claude-<uid>` and `/private/tmp/claude-<uid>` both exist, share one real path, and that directory has 7 entries. `CLAUDE_CODE_TMPDIR` was unset in this review process before the probes pointed it at a temp directory.
|
||||
|
||||
## lstart on this host
|
||||
|
||||
`ps -p <pid> -o lstart=` with `LC_ALL=C` prints five fields. With `TZ=UTC0` the string differs from the default-TZ string. `calendar.timegm` of the UTC string sits 0.367 s behind `time.time()` for a fresh `sleep`. `time.mktime` of the default-TZ string sits the same 0.367 s behind. macOS `ps` honors `TZ=UTC0`, so the UTC parse in `_session_file_live` (`claude.py:97-105`) is the process start, and a DST fold is the same instant in both readings.
|
||||
|
||||
On the six live session files, `_session_file_live` returned live for all six and stale for none. `lstart - startedAt/1000` is between −2.362 s and −0.771 s, so `lstart <= startedAt/1000 + 2` holds for every one of them.
|
||||
|
||||
A temp-home probe of the same function: a live pid with `startedAt` set to now, NaN, inf, `10**400`, a digit string, or boolean `True` is live. A dead pid is stale, including a dead pid whose `startedAt` is NaN (`ProcessLookupError` returns before the time check, `claude.py:78-81`). The same live pid with `startedAt` an hour earlier is stale.
|
||||
|
||||
## The three findings from the first review
|
||||
|
||||
Canonical uuid. `_canonical` (`claude.py:10-18`) returns the lowercase spelling only when the whole string is a uuid. `_canonical("memory")` is `None`, and an uppercase uuid comes back lowercase. On a temp config, `purge_artifacts("memory")` returned `[]`, residual was empty, `conversation_in_use("memory")` was false, and both `projects/<key>/memory/MEMORY.md` and `memory.jsonl` stayed. `purge_artifacts("....")` returned `[]`.
|
||||
|
||||
Linked directories. `_inside` (`claude.py:108-122`) walks `dirname(path)` with `lstat` and rejects a symlink component below the root, then requires the resolved parent to sit under the resolved root. A temp `<tmp-root>/<key>` symlink to an outside directory stayed a symlink, the outside `keep.txt` stayed, that linked path was listed by `residual_artifacts`, and the purged list had no outside path. The real `file-history/<uuid>` and the jsonl for that same id were removed once the holder was gone. A `file-history/<uuid>` symlink was unlinked and its target `secret.txt` stayed. A symlink child inside a real owned `<uuid>/` directory was removed with that directory, and the target file stayed.
|
||||
|
||||
Non-finite `startedAt`. The probe above: NaN, inf, and an int too large for float are live while the pid is alive, and a dead pid is stale. That is `claude.py:84-95`.
|
||||
|
||||
Uppercase spelling while a session file names a live pid: `conversation_in_use` of the uppercase id was true. `python -m lib_py.agents purge claude <UPPER> <ws>` exited 4 with `{"in_use": true, "purged": []}`, and `in-use` exited 4 with `What may hold it:` on stderr. The jsonl and `file-history/<uuid>` stayed (`__main__.py:146-148`).
|
||||
|
||||
A process that still has the lowercase uuid in its argv (`sh -c 'sleep 60; true' <uuid>`, argv0 `sh`, 57-character args, visible to `ps -eo pid=,ppid=,args=`) makes `conversation_in_use` true for the uppercase spelling and for the lowercase spelling (`claude.py:354`).
|
||||
|
||||
`purge_artifacts` itself has no in-use gate. It skips a live session file (`claude.py:323-327`) and still removes the other owned paths when called directly. The purge CLI and `stop_session.sh:290-298` both ask `conversation_in_use` first. Stop captures the in-use stderr with `2>&1 >/dev/null`, prints the records-preserved sentence, and exits 4. The passthrough `raise SystemExit(1)` is `stop_session.sh:327`. The stop SKILL.md exit-4 row (line 65) says the records are preserved and the session itself may already be down. `tests/data/skill_code_blocks.json` is unchanged since `6f4f40a` and still points that passthrough at `stop_session.sh:327`.
|
||||
|
||||
## Tests
|
||||
|
||||
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam916-916abd41-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped. The suite's own fixtures then point `HOME` at the test tmp.
|
||||
|
||||
On the snapshot, 149 tests ran in 39.61 s: 2 failed, 146 passed, 1 skipped. Modules: `tests/test_issue31_purge_residual.py`, `tests/test_a4_adapter_contract.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`. The skip is `test_claude_skills_link_resolves_to_agents_skills` (`tests/test_issue42_skill_blocks.py:266`), because `.claude/skills` is absent in this worktree. The issue-31 cases for a malformed id including `memory`, a linked directory, a linked orphan the CLI must report, NaN/`10**400`/bool `startedAt`, the UTC lstart mock, `_SYSTEM_TMP` staying inside the test, and the stop exit-4 text all passed. So did the a4 composite purge and the issue-55 home guard.
|
||||
|
||||
Both failures are `test_any_spelling_of_the_uuid_is_the_lowercase_session` (upper and mixed) at `tests/test_issue31_purge_residual.py:318`. The asserts above that line passed: the uppercase id was in use through the session file, purge and in-use both exited 4, and the owned files stayed. Line 315 then starts `sh -c "sleep 120" <lowercase-uuid>`. Bash 3.2 replaces a one-command `-c` script with that command. `ps` shows argv0 `sleep` and an 8-character argument string, and the uuid is gone. `conversation_in_use` of the uppercase id is false, which is what line 318 asserts against. The `sleep 60; true` form keeps the uuid and the same call returns true. The check at `claude.py:354` sees a command line that still carries the lowercase id. The process this test starts has already dropped it.
|
||||
|
||||
The same new tests copied onto an archive of `6f4f40a` fail on the pre-fix behavior. That archive is a separate clone. One line, `_SYSTEM_TMP = "/tmp"`, was added there so the new autouse fixture could patch a name `6f4f40a` does not define. `_tmp_roots` in that commit still hardcodes `/tmp` and does not read the name. `purge_artifacts("memory")` returned a `session-env/memory` path (`test_issue31_purge_residual.py:285`). The linked-dir purge returned removed paths (`:351`). The CLI asked about a leftover behind a link did not report that residual (`:373`). NaN, a digit string, and `True` were stale for a live pid (`:539`), and the live-holder purge was allowed through (`:556`). An uppercase id was outside `conversation_in_use` (`:307`). 21 failed, 13 passed, in 2.98 s. The real `/tmp/claude-<uid>` entry count stayed 7.
|
||||
|
||||
The in-use gate in `stop_session.sh` still runs for every agent that has a purge uuid, agy included. That was already true at `6f4f40a`.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
|
||||
|
||||
[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": "reviewer-grok-01",
|
||||
"job_id": "b1d3a736",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-17T15:16:54Z"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Review b1d3a736 — PR #59 third pass, Issue #31
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1` (`loop-20261017T145452Z-6c5aa6`). Head `de6f3fc81f8c33a9d2335320742dc5a47f566cda`, tree `ffc291e6fc93822070f2b9f1d17009e83f176fcf`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Versus base: 25 commits. Versus the second-pass head `2a8d73825b3832376aeaf96b09548359dc3cb887` there is one commit, `de6f3fc`, and it changes one file: `tests/test_issue31_purge_residual.py`, +3/−1. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
|
||||
|
||||
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. The second pass on `2a8d738` already measured the three Mac findings (canonical uuid, symlink boundary, non-finite `startedAt`) and the UTC `lstart` parse. This commit does not touch that product code. `conversation_in_use` still folds any spelling to the lowercase uuid and searches both that spelling and the one that was asked (`claude.py:350-354`).
|
||||
|
||||
## The holder keeps the uuid
|
||||
|
||||
`de6f3fc` replaces the stand-in at `tests/test_issue31_purge_residual.py:317`. The old line was `subprocess.Popen(["sh", "-c", "sleep 120", conv])`. macOS `/bin/sh` (bash 3.2) replaces a one-command `-c` script with `sleep`, and the uuid left the process list, so both spelling cases failed at the command-line assert. The new line is the same shape as `_holder` in `tests/test_issue10_13_purge.py:116`:
|
||||
|
||||
```python
|
||||
named = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])
|
||||
```
|
||||
|
||||
`sys` is the import already at the top of the file. The `try` / `finally` around it is unchanged: `named.kill()` and `named.wait()` are still `tests/test_issue31_purge_residual.py:321-323`.
|
||||
|
||||
A temp-home probe started that exact command with the control-plane interpreter (73-character path) and a fresh lowercase uuid. `ps -eo pid=,ppid=,args=`, the same invocation `conversation_in_use` uses, showed a 196-character argument string containing the uuid. The same string was complete with `COLUMNS=80` and with `COLUMNS=10000` (`COLUMNS` was unset in the probe process). The holder was still alive. `conversation_in_use` of the uppercase spelling and of the lowercase spelling were both true.
|
||||
|
||||
## Tests
|
||||
|
||||
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamb1-b1d3a736-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped.
|
||||
|
||||
On the snapshot, 149 tests ran in 42.05 s: 148 passed, 1 skipped, 1 error. Modules: `tests/test_issue31_purge_residual.py`, `tests/test_a4_adapter_contract.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`. The skip is `test_claude_skills_link_resolves_to_agents_skills` (`tests/test_issue42_skill_blocks.py:266`), because `.claude/skills` is absent in this worktree. Both `test_any_spelling_of_the_uuid_is_the_lowercase_session` cases passed, along with the rest of the issue-31 file.
|
||||
|
||||
The error is the session-scoped issue-55 guard at teardown of `test_r1_adapter_discover_writes_nothing_into_a_panes_claude_project_dir` (`tests/conftest.py:1603`). It reported 14 new entries, all `*.db-shm` or `*.db-wal` sidecars for 7 ids under the agy conversations directory. The guard left them in place. This is the known false positive from an agy conversation in another workspace, and it is not a verdict basis.
|
||||
|
||||
Regression of the spelling fold. A separate clone of this snapshot had one edit: the command-line search in `conversation_in_use` looked up the asked spelling only, while session files still used the lowercase id. The session-file asserts and the CLI exit 4 still passed. Both spelling cases then failed at `tests/test_issue31_purge_residual.py:320` (`a command line naming the lowercase id`), in 1.62 s. The new holder is what makes that assert see a live lowercase argv, so dropping the lowercase search fails the test. The clone was discarded. The snapshot was not modified (porcelain still empty after that run).
|
||||
|
||||
The three non-blocking notes from the second pass are unchanged, because this commit is the test line only: a linked `projects/<key>` is reported as residual, a lowercase query does not see an uppercase argv, and `purge_artifacts` itself has no in-use gate. The purge CLI and `stop_session.sh` still ask `conversation_in_use` first.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,47 @@
|
||||
# Review f0df1be0 — PR #59, Issue #31 claude purge
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1` (`loop-20261016T003524Z-a51548`). Head `6f4f40a4adb2115d5997dffd31fb6473f05cab68`, tree `f4d9422ad0c1d616b5f37065f3e96b34c28f78d8`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Thirteen commits, 12 files, +1127/−27. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
|
||||
|
||||
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. `bash -n` accepts `stop_session.sh` and `lib.sh`. With `set -u`, the new `CLAUDE_PROJECT_DIR` default (`lib.sh:78`) expands to `$HOME/.claude/projects` when both variables are unset, and to `$CLAUDE_CONFIG_DIR/projects` when only the config dir is set.
|
||||
|
||||
Every purge, `in-use`, and `stop --purge-conversation` exercised here used a temporary `HOME`, `CLAUDE_CONFIG_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_TMPDIR`, and `TMPDIR`. The real `~/.claude` and `/private/tmp/claude-<uid>` were listed with `ls`-level reads.
|
||||
|
||||
## What this Mac already stores
|
||||
|
||||
`~/.claude` has real directories `file-history`, `session-env`, `sessions`, `jobs`, and `projects`. `file-history` holds 18 uuid-named directories and no symlinks. `session-env` holds 21 uuid-named directories and no symlinks. `sessions` holds 5 `*.json` files and 5 key files; every key name matches `<pid>.<64 lowercase hex>.key`. Each json has integer `pid`, `sessionId`, and a millisecond `startedAt` (13 digits). The filename stem equals `pid` in all 5. All 5 pids were alive at the read. `jobs` holds 2 real directories, both 8 characters long, and each `state.json` `sessionId` starts with that directory name. No job entry is a symlink. `projects` has 16 keys, none of them symlinks; across those keys the entries include 25 `<uuid>.jsonl` files and 7 `<uuid>` directories, plus shared names such as the memory directory the purge leaves alone.
|
||||
|
||||
`/tmp` is a symlink to `/private/tmp`. `/tmp/claude-<uid>` and `/private/tmp/claude-<uid>` both exist and share one real path. Under that root, project keys are real directories. Their children are uuid directories (16) plus 3 other directories whose names are 57 or 58 alphanumeric characters, contain no uuid, and are siblings of the uuid scratchpads. `$TMPDIR/claude-<uid>` does not exist. `CLAUDE_CODE_TMPDIR` was unset in this review process. That is the layout `_tmp_roots` (`claude.py:25-48`) and `_owned_paths` (`claude.py:256-285`) assume: an existing per-uid root, then `<key>/<uuid>`.
|
||||
|
||||
## lstart on this host
|
||||
|
||||
`ps -p <pid> -o lstart=` with `LC_ALL=C` prints five fields. `time.strptime` of the collapsed line with `%a %b %d %H:%M:%S %Y` accepts both `Oct 5` and the BSD space-padded `Oct 5`. For the five live session files, `lstart - startedAt/1000` is between −2.43 s and −0.694 s, so `lstart <= startedAt/1000 + 2` (`claude.py:77`) holds for every one of them. Zero of those live files would be treated as a reused pid.
|
||||
|
||||
A temp-home probe of `_session_file_live`: the current pid with `startedAt` set to now is live; the same pid with `startedAt` 1.5 s after its own `lstart` is live; missing `startedAt`, `startedAt` 0, a non-numeric `startedAt`, `pid` 0, a negative pid, and boolean `True` are live. A dead pid is stale. The same live pid with `startedAt` an hour earlier, or 3 s earlier than its `lstart`, is stale. The process locale reported `en_US` / `UTF-8`, and the English month from `LC_ALL=C` parsed. A parse failure returns live (`claude.py:71-72`), which refuses the purge.
|
||||
|
||||
## Deletion stays on this session
|
||||
|
||||
On a temporary config root the purge removed the workspace `<uuid>.jsonl`, the sibling `<uuid>/` directory, `file-history/<uuid>/`, `session-env/<uuid>/`, `jobs/<uuid[:8]>/` whose `state.json` named the session, the dead `sessions/<pid>.json`, its `<pid>.<64 hex>.key`, and `<CLAUDE_CODE_TMPDIR>/claude-<uid>/<key>/<uuid>/`. The printed key label contains `<pid>.*.key` and the hex is absent from the purged list.
|
||||
|
||||
These stayed: another session's `sessions/<pid>.json` and key for a live pid, that session's scratchpad, `history.jsonl` whose body names the purged uuid, `projects/<key>/memory/MEMORY.md`, and a job directory whose 8-character name matches the uuid but whose `state.json` names a different session (`claude.py:244-254`). `_plausible_id` (`base.py:89-92`) rejects `""`, `.`, `..`, `*`, `../x`, `a/b`, and `....` (`".."` is a substring). Purging those ids left the tree unchanged.
|
||||
|
||||
Symlinks. A `file-history/<uuid>` symlink to an outside directory was unlinked; the outside `secret.txt` remained. A `projects/<key>/<uuid>` symlink to an outside directory was unlinked; the outside file remained. A directory symlink and a file symlink inside a real owned `<uuid>/` directory were removed with the directory; both targets remained (`_remove`, `claude.py:80-87`, uses `os.path.islink` and then `os.remove`; `shutil.rmtree` sees the real directory). A `jobs/<8>` symlink whose target `state.json` names the session was unlinked; the target directory and its `other.txt` remained (`_remove_job`, `claude.py:107-109`). A `sessions/<pid>.json` symlink to an outside json was unlinked after the read saw this session id; the outside file remained.
|
||||
|
||||
A project key under the temp root that is itself a symlink is followed: `<symlink>/<uuid>` was removed on the far side. That far-side directory is still the one named this session's uuid, which is the scratchpad path when a key has been relocated. The real per-uid temp root on this Mac has no key symlinks. The cross-session paths above stayed in the same probe.
|
||||
|
||||
A live session file is omitted from `_owned_paths` (`claude.py:274-278`). Calling `purge_artifacts` directly while that pid was alive left `sessions/<pid>.json` in place and removed `file-history/<uuid>/`. After the process exited, the same call removed the session file. The purge CLI does not take that direct path: `in-use` was true, the process exited 4, and the file-history marker was still there (`__main__.py:146-148`). `in-use` with no holder exits 0. `in-use` without a uuid exits 1.
|
||||
|
||||
## Stop
|
||||
|
||||
`stop_session.sh:285-300` runs `python -m lib_py.agents in-use <agent> <uuid> <cwd> --wait 5` before the registry write. Any status other than 0 exits 4 and leaves the row. The registry python then prints `purged:` lines and either `WARN residual:` or `residual: none (agent: <agent>)` (`stop_session.sh:355-368`). `lib.sh` exports `PYTHONPATH` to the skill dir at source time, so the new `in-use` module resolves the same way `exit-key` already does. `DELEGATE_JOB_ID`, `DELEGATE_JOB_TOKEN`, and `DELEGATE_JOB_ATTEMPT` are set before that call (`stop_session.sh:146-161`), including to empty strings, so `set -u` does not trip. The passthrough `raise SystemExit(1)` is now `stop_session.sh:327`, and `tests/data/skill_code_blocks.json` records that line. The stop SKILL.md diagram and the purge paragraph match the fenced blocks in that json (the skill-block test passed).
|
||||
|
||||
## Tests
|
||||
|
||||
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamf0-f0df1be0-work/pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped. The suite's own fixtures then point `HOME` at the test tmp.
|
||||
|
||||
On the snapshot, 80 tests passed and 1 skipped in 28.31 s: all of `tests/test_issue31_purge_residual.py` (32 tests), `tests/test_issue42_skill_blocks.py`, and `tests/test_issue55_agent_homes.py`. The skip is `test_issue42_skill_blocks.py:266`, because `.claude/skills` is absent in this worktree. The issue-31 file covers the owned-path purge, the untouched neighbor files, the same-prefix job, malformed ids, the live-process refusal, the reused pid, a key that cannot be unlinked, a job directory that cannot be emptied, `CLAUDE_CONFIG_DIR`, the temp-root rules, the stop exit 4, the stray-key `WARN residual:`, and codex, grok, and opencode residuals.
|
||||
|
||||
The same issue-31 file copied onto an archive of base `26e2c43` failed 24 tests and passed 8 in 17.09 s. The failures are behavioral. The old purge left `file-history/<uuid>` and `session-env/<uuid>` behind. The old purge CLI, asked about a live pid whose argv does not contain the uuid, removed the jsonl and returned `in_use` false. The old stop did not exit 4 for that live process and did not print `WARN residual:` for a transcript under another project key. Two failures are the new helpers `_session_file_live` and `_tmp_roots` being absent; the assertions above fail on the old behavior itself.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user