release(v4.9.4): the audit log follows a moved registry (Issue #57, PR #58) - archive the review reports and bump the lockstep version

Review loop (--review-only, --all-reviewer, focused on this Mac:
macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@63d8164) agy and
grok PASS, claude and codex NOT PASS (F1 paho via the /usr/bin/python3
fallback, F2 logs default path, R1 disk-fallback redispatch
interleaving, R2 symlinked registry), sent as a PR comment and fixed by
the author in four commits; round 2 (@a7cb90e) 4/4 PASS.

Full suite on macOS / Python 3.9.6 @a7cb90e: 1867 passed, 56 skipped,
1 error (the known #55 false positive: WAL/SHM of an agy conversation
running in another workspace). Real herdr panes and ~/.claude/projects
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-15 22:52:04 +09:00
1 parent a7cb90e63a
commit 7cf728e9f8
28 files changed
+619 -26

No files matched your search

@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261015T123502Z-1b9df6",
"base_sha": "e32c9574644a9185813a4dc881812694d62b6204",
"head_sha": "a7cb90e63abf454ccf22f6ff887cc25393755402",
"tree_sha": "96c5c5f82cb89e4383011d52fb1dcb21d4e787e6",
"review_commit": "a7cb90e63abf454ccf22f6ff887cc25393755402",
"reviewer": "planner-reviewer-claude-01",
"job_id": "b48be8a6",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-15T13:18:35Z"
}
@@ -0,0 +1,58 @@
# PR #58 (fix/issue-57) 병합 전 2차 리뷰 (job b48be8a6)
- 리뷰어: planner-reviewer-claude-01 (Reviewer)
- 리뷰 identity: loop-20261015T123502Z-1b9df6 / base e32c9574 .. head a7cb90e6 (커밋 15개; 1차 이후 대응 4개: 75c0042 F1, 151af99 F2, 1c598bb R1, a7cb90e R2) / tree 96c5c5f8
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261015T123502Z-1b9df6/1` — HEAD·tree 일치, `git status --porcelain` 0 줄, `.mam` 없음(시작·종료 모두). 본 체크아웃 HEAD e32c957(main) 변동 없음.
- 환경: macOS, `/usr/bin/python3` 3.9.6(paho 없음; 이 Mac 의 PATH `python3`), 체크아웃 `.venv` 3.9.6(paho 2.1.0), PATH `bash` = `/bin/bash` 3.2.57.
- 실행 안전: 테스트·탐침은 모두 `env -i`(PATH/HOME/LANG/TMPDIR 만, 필요한 값만 명시)로 브리프의 `-u` 목록 전부를 비웠고, basetemp 는 scratchpad 아래(이름에 "shim" 없음). 수동 재현은 scratchpad 또는 `/tmp/jb48-r2`(종료 후 삭제)의 임시 워크스페이스·cwd 에서만 했다. 수정 전 대조군은 `git archive HEAD` 복사본에 제품 파일만 되돌려 만들었다. 실제 `.mam`·`.mam.env`·모니터·herdr 서버·에이전트 홈 미접촉.
## 1. 1차 지적 4건 — 재현을 다시 돌린 결과
| 지적 | 1차 재현 (63d8164) | head (a7cb90e) | 수정 전 코드에서 회귀 테스트 |
|---|---|---|---|
| **F1** paho 없는 폴백에서 control_plane·status 실패 | `AGENT_PYTHON_BIN=/usr/bin/python3` 로 `mam_control_plane_json` rc=1 `ModuleNotFoundError: paho`; `get_job_status` 예외 | rc=0, 바인딩 JSON 에 로그 루트 포함; `status.sh` 의 `get_job_status` 두 복사본 모두 `('abc12345', 'unknown')` | `test_control_plane_binding_works_without_paho`, `test_status_works_without_paho[json/text]` **실패**(paho 미존재로 control_plane·service import 실패) |
| **F2** 래퍼 `logs` 기본 경로가 cwd 상대 | `WORKSPACE_ROOT=ws`, cwd=elsewhere 에서 `logs --list` 가 잡을 못 찾음 | `logs --list` 가 ws 의 잡을 나열, `logs <id>` 에 `registered` 이벤트, `--registry-dir` 를 주면 그 레지스트리를 따름, elsewhere 에 아무것도 안 생김 | `test_logs_without_registry_dir_follows_the_workspace_from_any_cwd` **실패**(`(no logged jobs under …)`) |
| **R1** 디스크 폴백이 이후 디스패치의 terminal 을 이전 시도로 오인 | (codex 지적) | 감사 상태 채택 직전에 레코드를 다시 읽어 `(dispatch_token, attempt_id, transition_seq)` 가 처음 읽은 것과 같을 때만 채택 | `test_disk_fallback_rejects_a_terminal_that_belongs_to_a_later_dispatch` **실패**(`B's completion was attributed to A`) |
| **R2** jobs 디렉터리가 심볼릭 링크일 때 submit 과 logs 불일치 | (codex 지적) | `/tmp/jb48-r2/alias/jobs -> actual/jobs` 로 `submit --dry-run`: 출력 경로·감사 로그 모두 `actual/delegate_job_logs/<id>`; `logs --registry-dir alias/jobs` 와 `registry.py --registry-dir actual/jobs logs --list` 가 같은 잡을 찾음 | `test_a_symlinked_registry_dir_resolves_to_one_audit_log` 는 63d8164 에서는 `logroot` 부재로 ImportError 라 증거가 안 되므로, **a7cb90e^ (logroot 있음, `abspath` 규칙)** 로 다시 돌려 **실패**(`…/alias/… ≠ …/actual/delegate_job_logs`) 확인 |
## 2. 수정이 새 문제를 만들지 않았는가
**F1 (모듈 이동)**
- `lib_py/delegation/logroot.py` 는 stdlib(`os`, `typing`)만 쓴다. `mqtt_common` 이 `DEFAULT_REGISTRY_DIR`·`_workspace_mam_dir`·`default_registry_dir`·`get_logs_dir`·`sibling_logs_dir` 를 그대로 재노출하므로 기존 import 경로(`mqtt_common.get_logs_dir` 등)는 불변이다.
- `from lib_py.delegation.logroot import …` 는 패키지 `__init__` 을 거쳐 `daemon`·`service`·`registry`·`publish_event`·`publisher` 까지 로드한다. 이 Mac 의 `/usr/bin/python3` 로 직접 import 해 보니 12 개 `lib_py` 모듈이 모두 올라오고 `paho` 는 로드되지 않았다. 직접 paho 를 import 하는 곳은 `mqtt_common`·`publisher` 둘뿐이고 둘 다 `try/except ImportError` 로 감쌌으며 `from __future__ import annotations` 라 `-> mqtt.Client` 주석도 import 시 평가되지 않는다. 실제로 브로커가 필요하면 `make_client` 가 명확한 `ImportError` 를 낸다.
- eager import: `logroot` 는 `eager.CORE_MODULES` 에 없지만 목록에 있는 `mqtt_common` 이 import 시점에 로드하므로 동결 런타임에 함께 들어간다(아래 비차단 메모).
**F2**: `local -a reg=()` + `${reg[@]+"${reg[@]}"}` 를 `/bin/bash -u` 로 직접 확인 — 빈 배열이면 인자 없음, 주어지면 `--registry-dir <dir>`. 래퍼의 다른 subcommand 는 바뀌지 않았다.
**R1 (재읽기)**
- 경합: B 의 terminal 감사 상태는 재디스패치(레코드 토큰·seq 변경)가 커밋된 뒤에만 생긴다. 그러므로 감사 읽기에서 B 의 terminal 이 보였다면 레코드는 이미 그 전에 바뀌었고, 그 뒤의 재읽기가 반드시 차이를 본다. 재읽기 이후의 변경은 이미 읽은 감사 상태와 무관하다.
- 같은 시도 안에서 정상 전이(A running → A completed)가 두 읽기 사이에 끼면 seq 가 달라져 "deferred" 로 한 라운드 늦어질 뿐이고, 다음 라운드는 레코드 자체의 terminal 을 읽는다.
- 성능: 감사 상태가 terminal 이고 stale 이 아닐 때만 레코드 하나를 더 읽는다. 무시할 만하다.
**R2 (realpath)**
- 기본 경로: `WORKSPACE_ROOT=/tmp/...`(macOS 에서 `/tmp → /private/tmp`)일 때 `get_logs_dir()` 은 이제 `/private/tmp/.../.mam/delegate_job_logs` 문자열을 돌려준다. 물리적으로는 이전과 같은 디렉터리임을 확인했다. 결과를 문자열로 비교하는 곳은 `control_plane.forwarded_keys`(이제 양쪽 모두 `_abs`=realpath)와 루프 러너의 제외 목록(`runner.py` 가 `d.resolve().relative_to(root.resolve())`)뿐이라 영향이 없다.
- `forwarded_keys` 탐침: `/tmp`·`/private/tmp` 혼합 두 방향, 후행 `/`, 심볼릭 링크 alias 경유 모두 형제로 판정되어 전파하지 않는다. 비형제만 전파한다. 1차에서 비정규 입력이 "전파" 쪽으로 어긋나던 비대칭도 사라졌다.
- `from_env({"WORKSPACE_ROOT": "/tmp/..."})` 의 기본 바인딩은 `defaults()` 와 같아서, 평소 실행은 pane 에 아무것도 전파하지 않는다.
- 레지스트리 디렉터리 자체가 심볼릭 링크인 경우에만 감사 로그가 링크 대상 옆으로 간다. 이는 R2 가 의도한 동작이며, 래퍼(`pwd -P`)와 바인딩(realpath)이 이미 그렇게 보고 있었다.
## 3. 이 Mac 정합성
- 변경 py 15 개: `ast.parse(feature_version=(3,9))` 와 `/usr/bin/python3 -m py_compile` 모두 통과. 3.10+ 문법·API 없음.
- `lib.sh`·래퍼·`status.sh`: `/bin/bash -n`(3.2) 통과.
## 4. 테스트 (head, 검증 경로)
| 묶음 | 결과 |
|---|---|
| A: issue57·15·21_acceptance·21_worktree_prereqs·tier1·20·17_item4·17_workspace_root_isolation·36·14 | **475 passed, 2 skipped** |
| B: issue18·6_fail_closed·34·33·5_phase1/2a/2b/4_runtime·tier2·12·23_24·21_phase2·b7 | **493 passed** |
`5_phase4_runtime` 의 eager 모듈 검사도 통과했다. 어느 실행도 검증 경로에 `.mam` 을 남기지 않았고 #55 가드 오탐도 나오지 않았다.
## 5. 비차단 메모
- `lib_py.delegation.logroot` 를 `eager.CORE_MODULES` 에 명시하면 의도가 분명해진다. 지금도 `mqtt_common` 을 통해 로드되므로 기능 문제는 없다.
- paho 없이 `publisher` 의 브로커 경로를 직접 타면 `make_client` 의 `ImportError` 가 먼저 나는 구조다. 다른 진입점이 `mqtt.` 를 먼저 건드리면 `AttributeError: 'NoneType'` 로 보일 수 있지만, 브로커가 필요한 경로는 원래 paho 가 필수다.
## 결론
1차 지적 4건(F1·F2·R1·R2)이 모두 해소됐다. 1차 재현을 다시 돌려 확인했고, 각 회귀 테스트가 수정 전 코드에서 의도한 이유로 실패한다(R2 는 직전 부모 커밋 기준). 모듈 이동·재읽기·realpath 는 기본 경로, 바인딩·전파, 기존 import 경로에 새 문제를 만들지 않으며, 이 Mac 의 Python 3.9·bash 3.2 에서 정합하다.
[VERDICT: PASS]
@@ -0,0 +1,67 @@
# PR #58 (fix/issue-57) 병합 전 리뷰 — 이 Mac 정합성 중심 (job b9c7522d)
- 리뷰어: planner-reviewer-claude-01 (Reviewer)
- 리뷰 identity: loop-20261014T152849Z-f1a415 / base e32c9574 .. head 63d81646 (커밋 11개, 21 파일) / tree e8f162f7
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261014T152849Z-f1a415/1` — HEAD·tree 일치, `git status --porcelain` 0 줄, `.mam` 없음(시작·종료 모두). 본 체크아웃 HEAD e32c957(main) 변동 없음.
- 환경: macOS, `/usr/bin/python3` 3.9.6(paho 없음 — 이 Mac 의 PATH `python3` 도 이것), 체크아웃 `.venv` 3.9.6(paho 2.1.0), PATH `bash` = `/bin/bash` 3.2.57.
- 실행 안전: 모든 테스트·탐침은 `env -i`(PATH/HOME/LANG/TMPDIR 만, 필요한 값만 명시)로 브리프의 `-u` 목록 전부를 비웠고 basetemp 는 scratchpad 아래(이름에 "shim" 없음). 수동 재현은 scratchpad 의 임시 워크스페이스·cwd 에서만 했고, base 비교는 `git archive e32c957` 복사본으로 했다. 실제 `.mam`·`.mam.env`·모니터·herdr 서버·에이전트 홈 미접촉.
## 1. 결과 요약
| 항목 | 결과 |
|---|---|
| Python 3.9 문법(`ast.parse(feature_version=(3,9))`, 변경 py 13 개)·`py_compile` | 통과. 3.10+ 문법·API(match, `X \| None`, 내장 제네릭 주석, `removeprefix`, `tomllib`, `zip(strict=)`) 사용 없음 |
| bash 3.2 `bash -n`(lib.sh, 래퍼, worktree_prepare, reconcile, status) | 통과. 래퍼의 `LOGS_DIR_ABS=""` 전역 초기화, `var=$(…) \|\| {…}` 실패 처리, `env_add+=(…)` 모두 3.2 `set -u` 안전 |
| head 관련 모듈 A: issue57·15·20·21_acceptance·21_worktree_prereqs·tier1·17_item4·17_workspace_root_isolation·b7·36 | **477 passed, 2 skipped** |
| head 관련 모듈 B: issue14·18·6_fail_closed·34·33·5_phase1/2a/2b·tier2·12·23_24·21_phase2·**10_13_purge** | **503 passed**(알려진 실패 #28 의 `test_issue10_13_purge` 도 이 Mac 에서 통과) |
| 수정 전 대조군(제품 파일만 base 로 되돌린 복사본) + 새 `test_issue57_logs_follow_registry.py` | **15 failed, 6 errors, 1 passed** — 회귀를 실제로 잡는다 |
| 이전 라운드(ee7cef33) 차단 지적: `test_issue15…:64`, `test_tier1_unit…:512` | 두 곳 모두 `monkeypatch.delenv("MAM_JOBS_DIR")` 추가로 해소, 통과 |
## 2. 맞게 된 부분
- **규칙은 한 곳**: `mqtt_common.sibling_logs_dir`(순수) + `get_logs_dir(registry_dir)`(`DELEGATE_JOB_LOGS_DIR` > 레지스트리 형제). 쓰기·읽기 지점(register·pick·legacy/engine 미러·conflict·publish·subscriber·remote·reconcile·runner·lib.sh·래퍼)이 모두 이 함수를 거친다. lib.sh 의 bash 기본값 사본은 `delegate_logs_dir` 로 대체됐고, 해석 실패 시 감사 확인만 건너뛰고 `events.log` 증거는 유지한다.
- **macOS 경로**: `control_plane._abs` 는 `realpath(expanduser())` 라 `from_env`·행 기록이 모두 `/private/tmp`·`/private/var` 로 정규화된다. `forwarded_keys` 를 직접 탐침: 형제(정규화 후 동일) → 전파 안 함, `/tmp` 와 `/private/tmp` 가 섞인 비정규 입력·후행 `/`·비형제 → 전파. 어긋날 때는 늘 "전파" 쪽(기존 동작)이라 안전하다.
- **디스크 폴백의 재디스패치 안전성**: `job_subscriber._check_disk_fallback` 은 이제 `status.json` 의 `status` 필드를 읽고(이전엔 dict 를 비교해 아무것도 맞지 않았음), 버전 있는 레코드는 감사 `transition_seq` 가 같거나 새로울 때만 채택한다. 이전 시도의 terminal 감사 상태가 새 시도를 끝내지 못하며(`test_old_audit_terminal_does_not_complete_the_redispatched_attempt`), 손상된 seq 는 stale 로 처리한다. 버전 없는 레코드·레코드 없음은 기존 규칙.
- **래퍼**: 16 곳의 `${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}` 제거, 해석 실패 시 fail-closed(exit 1), 해석값을 export 해 자식과 주입 명령이 같은 루트를 쓴다.
- **worktree_prepare / control_plane**: 호출자가 명시한 `DELEGATE_JOB_LOGS_DIR` 만 넘기고, 유도 가능한 형제 루트는 행에만 기록한다. 기존 행 중 레지스트리를 옮겼지만 로그가 워크스페이스에 있던 것은 비형제라 계속 전파되어 resume 짝이 유지된다.
## 3. 차단 지적
**F1. 경로 헬퍼 하나 때문에 create/resume 바인딩과 status 가 paho 를 요구하게 됐다 — 이 Mac 의 문서화된 폴백에서 회귀.**
- `lib_py/control_plane.py:28` 이 `from lib_py.delegation import mqtt_common` 을 새로 하고(base 의 control_plane 은 stdlib 만), `status.sh:171,383` 이 `get_job_status` 안에서 `from lib_py.delegation.mqtt_common import sibling_logs_dir` 를 무조건 한다. `mqtt_common.py:33` 은 모듈 최상단에서 `import paho.mqtt.client` 다.
- 두 경로의 인터프리터는 `_delegate_py_bin`(`lib.sh:2111-2130`): `AGENT_PYTHON_BIN` > 활성 venv > 스킬 위쪽 가장 가까운 `.venv` > **PATH `python3`**. README 도 이 폴백을 문서화한다. 이 Mac 의 PATH `python3` 은 `/usr/bin/python3` 3.9.6 이고 paho 가 없다(install.sh 는 시스템 python3 에 PyYAML 만 요구).
- 재현(임시 워크스페이스, herdr 미호출):
```
AGENT_PYTHON_BIN=/usr/bin/python3 로 lib.sh source 후 mam_control_plane_json <wt>
base: rc=0
head: rc=1 ModuleNotFoundError: No module named 'paho'
status.sh 의 get_job_status 를 그대로 떼어 /usr/bin/python3 로 job 이 있는 행에 실행
base: ('abc12345', 'unknown')
head: ModuleNotFoundError: No module named 'paho'
```
- 영향: 그 폴백에서 `create_session.sh:232-238`·`resume_session.sh:100-106` 은 워크트리 대상이면 바인딩 계산 실패로 **exit 2**(거부), 제어 루트면 `control_plane` 없이 기록한다. 읽기 전용 `status.sh` 는 위임 잡이 걸린 행이 하나라도 있으면 heredoc 이 예외로 죽는다. 둘 다 base 에서는 paho 없이 동작했다.
- 수정(작음): `sibling_logs_dir` 를 paho 를 import 하지 않는 모듈(예: `lib_py/paths.py` 또는 새 `lib_py/delegation/logroot.py`)로 옮기고 `mqtt_common` 은 그것을 재노출, `control_plane`·`status.sh` 는 그 모듈에서 import. (또는 `mqtt_common` 의 paho import 를 지연.) 회귀 테스트로 "paho 없는 인터프리터에서 `control_plane json`·`get_job_status` 가 동작" 을 두면 좋다.
**F2. 래퍼 `logs` 의 기본 경로가 바뀌었다 (레지스트리를 옮기지 않은 평소 실행).**
- `multi-agent-mux-delegate-job` `cmd_logs` 는 이제 항상 `--registry-dir "$reg"` 를 넘기고 `reg` 의 기본은 `REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}"`(`:78`) — cwd 상대다. base 는 `--registry-dir` 없이 `registry.py logs` 를 불러 `get_logs_dir()` 이 `$WORKSPACE_ROOT/.mam/delegate_job_logs` 를 썼다(registry.py 자체의 기본 `default=default_registry_dir()`(`:370`) 도 `WORKSPACE_ROOT` 를 존중한다).
- 재현(임시 디렉터리, `WORKSPACE_ROOT=ws` 에 잡 하나 등록, cwd=`elsewhere`):
```
base: logs --list → 그 잡을 나열
head: logs --list → "(no logged jobs under <elsewhere>/.mam/delegate_job_logs)"
```
- 수정(작음): `--registry-dir` 가 주어졌을 때만 넘긴다(이전 라운드 구현처럼 `local reg=()` 배열 + `${reg[@]+"${reg[@]}"}`). 그러면 registry.py 의 기본(`MAM_JOBS_DIR` > `WORKSPACE_ROOT` > cwd)이 그대로 쓰여 기본 경로가 불변이다. "cwd≠워크스페이스에서 `logs`" 테스트 한 건을 권한다.
두 지적 모두 한두 줄 수정이며 설계 변경은 필요 없다.
## 4. 비차단 메모
- `docs/design/worktree-creator-model.md` 의 status 행(“`MAM_JOBS_DIR`·행 `mam_jobs_dir`·감사 로그(`DELEGATE_JOB_LOGS_DIR`·행 `delegate_job_logs_dir`)·기존 후보 순 조회”)에 새로 추가된 "레지스트리 옆 감사 로그" 후보가 빠져 있다.
- `control_plane.forwarded_keys` 는 로그 쪽 값을 정규화 없이 비교한다. 실제 흐름에서는 양쪽이 이미 realpath 라 문제없고 어긋나도 전파 쪽이라 안전하지만, 양쪽에 `_abs` 를 걸면 의도가 분명해진다.
- `cmd_logs` 의 `--registry-dir` 뒤 인자가 없으면 `set -u` 로 "unbound variable" 종료 — fail-closed 이므로 무방.
- 수정 전 대조군의 6 errors 는 base 코드가 테스트가 기대하지 않는 위치에 쓰면서 생긴 setup/teardown 오류로, head 에서는 없다.
## 5. 잔류물
- 복사본·basetemp·탐침 디렉터리는 모두 scratchpad 아래. 검증 경로는 깨끗(`.mam` 없음, `git status` 0 줄). #55 가드 오탐은 이번 실행에서 나오지 않았다.
## 결론
규칙의 일원화, 쓰기·읽기 지점, macOS 경로 정규화, 디스크 폴백의 재디스패치 안전성, bash 3.2·Python 3.9 이식성, 테스트의 회귀 탐지는 모두 맞다. 그러나 (F1) 경로 헬퍼를 paho 를 끌어오는 모듈에서 import 해 이 Mac 의 문서화된 `python3` 폴백에서 create/resume 바인딩과 status 가 깨지고, (F2) 래퍼 `logs` 의 기본 경로가 cwd 상대로 바뀌었다. 둘 다 base 대비 재현됐고 작은 수정으로 끝난다.
[VERDICT: NOT PASS]