Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-2b62056b.md
T
GodopuandClaude Fable 5.1 d34b532ccf release(v4.6.0): bump framework and eight skills to 4.6.0 in lockstep; archive PR #22 merge-side reviews
Issue #21 phase-1 prerequisites (PR #22) add loop runner options and the
control-plane module: MINOR. VERSIONS.md gains the v4.6.0 entry, the
archived design reviews lose their markdown trailing whitespace so
`git diff --check` is clean, and the three merge-side PASS reports
(claude, agy, codex @ 4a81d15) are archived under .agents/reports/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-09-14 21:53:51 +09:00

192 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔍 PR Review — Job 2b62056b (PR #22 `feat/issue-21-worktree-prereqs` @ `4a81d15`, base `main` `5611859`)
- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) · 2026-09-14
- **대상**:
- 워크트리 `multi-agent-mux-wt/pr-22`(detached `4a81d15`)
- `git log 5611859..HEAD` 9커밋, `git diff 5611859...HEAD` 19파일 +1905/−78
- 설계 커밋 `fe96db1`·`dcf2fab` 과 구현·교정 커밋 7개(`d3d3a76` → `4a81d15`)
- **방법**:
- 워크트리 diff 를 정독하고 이슈 #21·PR 본문과 대조했다.
- 임시 디렉터리에서 실험 1건을 했다(공백 경로 명령 실행).
- 워크트리에서 **전체 스위트**를 읽기 전용 조건(`PYTHONDONTWRITEBYTECODE=1 -p no:cacheprovider`)으로 실행했다. 실패는 base(본 체크아웃 `5611859`)에서 같은 시험으로 대조했다.
- 코드는 수정하지 않았다.
- **안전 규칙 준수**:
- 본 체크아웃은 `main` `5611859` 를 유지했다(checkout·switch·merge 없음). 워크트리의 추적 파일은 바꾸지 않았다(HEAD·`git status` 전후 동일). 다만 스위트 실행 중 일부 시험이 무시 대상 `.mam/`(`delegate_job_logs`·`shim`)을 워크트리에 만들었다. 기존 #20 위생 문제이며 §7 에 적었다.
- 실제 `.mam`·monitor 데몬·에이전트 세션은 조작하지 않았다. push·merge·이슈/PR 상태 변경도 없었다.
## 0. 요약
| # | 항목 | 판정 |
|---|---|---|
| (1) | 래퍼 절대경로화 | ✅ 공백 경로·다른 cwd 에서도 동작한다(실험 E1) |
| (2) | 루프 러너 `--worktree`/`--base`/`--review-only` | ✅ 소스 루트와 제어 루트가 정확히 나뉜다. 빈 변경분 가드는 diff 수집과 같은 정의라 문서만 바뀐 경우를 오탐하지 않는다 |
| (3) | `control_plane.py` 와 create/resume/stop/status/reconcile | ✅ 바인딩 없는 기존 행과 기본 워크스페이스는 동작이 바뀌지 않는다(하위 호환) |
| (4) | `lib.sh`·shim 계약 시험 | ✅ `_sks_*`·`send_keys_safe` 를 건드리지 않았다. h25 는 오히려 강화되었다 |
| (5) | 설계 문서 정정 | ✅ codex NOT PASS 사유 4가지를 반영했다. 비차단 보강 1건 |
| (6) | 신규 시험 결정성·격리 | ✅ tmp/sandbox 만 쓰고 브로커가 필요 없다. 위생 비차단 1건 |
| (7) | 스위트 결과로 회귀 확인 | ✅(비차단) 937 passed / 4 failed. 4건 모두 기존 T-1 시간 임계(공개 브로커) 시험이고, base 에서도 같은 부류가 실패한다. PR 회귀 없음 |
| (8) | #21 항목 완료·범위 | ✅ 1·2·3·6 완료, 4·5·7·8 은 타당하게 제외됐다. 후속 권고 2건 추가 |
| (9) | 알려진 미비 (a)(b) 외 차단 결함 | 없음. (a)(b)는 확인했다(§9). 나머지는 비차단 권고뿐이다 |
## 1. (1) 래퍼 절대경로화 (`d3d3a76`)
- **레지스트리**:
- `REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}"` 로 이제 `MAM_JOBS_DIR` 을 존중한다.
- `cmd_submit` 은 `cd "$WORKDIR"` 직후 `_abs_registry_dir` 로 `cd -P … && pwd -P` 절대화하고 `SKS_REGISTRY_DIR` 로 export 한다. 상대 레지스트리는 종전처럼 `WORKDIR` 기준이다.
- **명령 문자열**: `_pub_cmd` 가 모든 토큰을 `printf %q` 로 인용한다.
- 감사 로그 루트는 구독자와 같은 해석기(`get_logs_dir`)의 절대경로를 `DELEGATE_JOB_LOGS_DIR=%q` 접두로 고정한다.
- `_instructions` 는 `%s` 자리표시자만 쓰므로 경로의 `%` 로 인한 포맷 주입이 없다.
- **경로 목록**:
- 브리프 경로: `$REGISTRY_DIR/$JOB_ID/brief.md`
- Output Report Path(direct·loop 두 곳)
- 상대 `PY` 를 `cd` 전에 절대화
- create 의 onboarding 발행 명령(`create_session.sh` delegate 블록)
- **실험 E1** — 레지스트리 `…/reg a b/jobs`, 감사 로그 `…/logs dir`, 모두 공백 포함. 주입된 명령을 **다른 cwd** 에서 그대로 `eval` 했다(브로커는 닫힌 포트).
| 확인 | 결과 |
|---|---|
| 시작 명령 | `DELEGATE_JOB_LOGS_DIR=…/logs\ dir …/python …/publish_event.py --registry-dir …/reg\ a\ b/jobs --job … --event started --heartbeat 30` |
| 시작·성공 명령 rc | 2, 2(브로커 불가, 로컬 기록 뒤) |
| 공백 레지스트리의 events | `['started', 'completed']`, 레코드 `completed` |
| 감사 로그 루트 | 지정한 `logs dir` 아래에 job 디렉터리 |
| 다른 cwd 에 `.mam` 생성 | 0 |
| heartbeat 타이머 잔여 | 0(터미널 발행이 멈춤) |
- 기존 주입 문구 계약: `test_t1_4` 가 소스에서 `.mam/jobs/$JOB_ID` 리터럴을 금지한다. issue14·issue6·issue18 시험 결과는 §7.
- 남는 제한: 작은따옴표가 든 경로는 지원하지 않는다(주입 문구가 명령을 `'…'` 로 감싸는 prose 형식). 문서 §7 에 적혀 있다.
## 2. (2) 루프 러너 (`d907359`)
- **의미와 상호작용**:
- `--worktree` 는 소스 루트다. 경로가 디렉터리이고, `--show-toplevel` 이 자기 자신이며, `git-common-dir` 가 제어 루트와 같아야 한다(`runner.py` `_validate_worktree`).
- `--base` 는 소스 HEAD 의 조상인 커밋이어야 한다(`_resolve_base`). env 폴백은 없다(D17).
- `--review-only` 는 `--base` 가 필수이고 `--plan` 과 배타다.
- `MAM_LOOP_WORKTREE` env 는 CLI 가 우선이다.
- **루트 분리**:
- **소스 루트 기준**: diff(제어 루트의 `diff_collect.sh` 를 소스 cwd 에서 실행 — 워크트리의 미검토 수집기는 실행하지 않음), `base_commit`(`--base` 또는 Phase 2 시작 시 소스 HEAD), head, 변경분 가드.
- **제어 루트 기준**: wrapper 경로, 래퍼에 넘기는 `--registry-dir self.jobs_dir`, `wait_for_job`·`find_report`·cleanup 의 `jobs_dir`, 체크포인트(`worktree_root`·`head_commit` 필드 추가), loop guard, 리포트 승격.
- `jobs_dir` 는 `MAM_JOBS_DIR` 을 존중한다. 상대값은 제어 루트 기준이며, 래퍼 해석과 같다(`t2_15`).
- **빈 변경분 fail-closed**:
- `git diff --quiet <base>`(추적 파일의 작업 트리 대 base)와 `ls-files -o --exclude-standard`(미추적)로 판정한다. 이는 `diff_collect.sh` 가 수집하는 내용(`git diff "$base"` + 미추적 파일, `:40-48`, `:73-74`)과 같은 정의다.
- 따라서 **문서만 바뀐 리뷰 전용 흐름도 `present`** 로 판정된다. 오탐이 없다.
- `empty` 는 base 대비 추적 변경과 미추적 파일이 모두 없을 때뿐이다. 무시 파일만 바뀐 경우는 리뷰할 것이 없으므로 거부가 맞다.
- git 오류는 `unknown` 이 되어 역시 거부된다. 가드는 `--worktree`/`--base` 가 있을 때만, 리뷰 반복마다 동작한다.
- **`run_loop.sh`**:
- 새 인자는 기존 `MAM_LOOP_ARGV` 로 Python 러너에 그대로 전달된다. freeze 재실행도 원래 argv 를 넘긴다.
- 레거시 `MAM_LOOP_PY=0` 은 새 옵션(과 `MAM_LOOP_WORKTREE`)을 freeze·loop lock **이전에** exit 1 로 거부한다(`t2_8`, `mam_sandbox` 에서).
- 참고: 가드는 "작업 트리 대 base" 이고, 브리프의 `[워크트리 컨텍스트]` 는 커밋 수와 미커밋 변경 유무를 함께 보여 준다. 따라서 커밋 0개·미커밋 변경만 있는 상태도 리뷰어에게 드러난다.
## 3. (3) `control_plane.py` 와 전파 경로 (`ee1d896`·`af8a54c`·`13259ed`)
- **스키마**: 행 필드 `control_plane = {workspace_root, agent_sessions_yaml, mam_jobs_dir, delegate_job_logs_dir}`, 모두 realpath 다.
- **해석 순서**: `describe()` 는 행 바인딩(완전할 때)을 호출자 env 보다 우선하고, 다르면 stderr 로 경고한다. `forward` 는 `--workspace` 의 기본 바인딩과 다를 때만 참이다.
- **전파**:
- create·resume 은 `K=V ` 접두를 `CMD_FULL` 앞에 붙인다. `lib.sh` new-session 이 이것을 `--env` 로 바꾸는 기존 경로를 쓴다.
- `13259ed`/`af8a54c`: env 플래그가 있으면 **idle pane 을 재사용하지 않고** `pane split`/`workspace create` 호출에 `--env` 를 싣는다(`lib.sh` 775-787 부근).
- 재사용 pane 에 `env K=V` 를 `agent start --` 뒤 에이전트 인자로 넘기던 종전 결함(R1)이 없어졌다.
- 공백이 든 바인딩을 전파해야 하면 rc 3 → create·resume 이 exit 2 로 거부한다(`lib.sh` 가 `--env` 플래그를 인용 없이 나누므로).
- **resume**: 행 바인딩을 다시 전파한다. 바인딩 없는 기존 행은 이번 resume 의 바인딩으로 채운다(`update_yaml_resumed.sh`).
- **stop**: env 가 없을 때만 행의 `mam_jobs_dir`·`delegate_job_logs_dir` 를 export 한다. 취소 발행과 감사 로그가 행의 제어 평면으로 간다(R3).
- **status·reconcile**:
- `MAM_JOBS_DIR`·행 바인딩·감사 로그를 먼저 보고, 그 뒤 종전 후보를 그대로 본다.
- reconcile 구독 루프는 `MAM_JOBS_DIR` 을 우선한다(`reconcile.sh:239-245`). 이 부분의 시험은 소스 정적 가드뿐이다(`t3_7`).
- **하위 호환**:
- `--workspace` 가 제어 루트와 같은 일반 세션은 바인딩이 기본값과 같다. 그래서 기록만 되고 전파되지 않으며(`t3_2`), pane 생성 경로(idle pane 재사용 포함)가 종전과 같다.
- 바인딩 없는 기존 행은 status·stop·reconcile 에서 종전 후보 순서로 처리된다.
- 비차단: `mam_control_plane_json … 2>/dev/null || true`(create)가 실패를 삼키면 바인딩 없이 종전 동작으로 조용히 떨어진다. PR 후속 1·2 와 같다.
## 4. (4) `lib.sh`·shim 계약 시험
- **`lib.sh` 변경은 두 곳뿐이다**:
- new-session 의 idle pane 재사용 조건에 `&& [ -z "$env_flags" ]` 를 추가하고, 재사용 pane 에 `env …` 를 붙이던 코드를 제거했다.
- 파일 끝에 얇은 진입점 `mam_control_plane_json`·`mam_control_plane_prefix` 를 추가했다.
- `_sks_*`·`send_keys_safe` 를 포함한 다른 hunk 는 없다.
- **h25 는 약화가 아니라 강화다**:
- 종전 h25 는 결함 있는 동작(idle pane 재사용 + 에이전트 인자로 env)을 고정하고 있었다.
- 새 h25 는 env 가 **생성 호출의 `--env`** 에 있고 `agent start --` 인자에는 없음을 단정한다.
- env 없는 재사용은 h24 가 그대로 고정한다.
## 5. (5) 설계 문서 정정 (`b61c8b1`)
- **codex NOT PASS 사유별 반영**:
- 부분 지원: §3 제목이 "현재 코드 기준" 에서 "부분 지원 + 1단계 수정 반영" 으로 바뀌었다.
- 루트 3종: §7 이 제어·소스·런타임 코드 루트를 정의한다.
- 리뷰 identity: §2-C 가 항목 5 로 미뤘다고 명시한다.
- 과장 표현: A 의 "완전 격리", 리뷰어 "읽기 전용", 잡 토픽과 지문을 정정했다(§1 표).
- **추가된 내용**:
- 스킬별 호환성 표(§7)
- 1단계 운영 형태(구현은 루프 밖, 리뷰는 `--review-only` 로 하나씩)
- 두 번째 루프는 **거부**된다는 서술
- "실제 herdr `--env` 전파는 mock 수준까지만 검증" 이라는 정직한 단서
- 비차단 보강: §3 의 create 레시피에 `MAM_NO_SUBSCRIBER_DAEMON=1` 을 넣을 것.
- `create_session.sh:567` 의 `start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE"` 는 여전히 워크트리의 `reconcile.sh` 로 데몬을 띄울 수 있다(항목 4 에서 기본화 예정).
- "모니터는 본 체크아웃에서만"(§3 본문)을 지키려면, 항목 4 가 오기 전까지는 레시피 한 줄로 막는 편이 안전하다.
## 6. (6) 신규 시험 780줄의 결정성·격리
- 파일 머리의 규칙대로 모든 쓰기가 `tmp_path`·`mam_sandbox` 아래에서 일어난다.
- `wt_repo` 픽스처는 `monkeypatch.setenv` 로 `WORKSPACE_ROOT`·`MAM_JOBS_DIR` 을 임시 루트에 묶는다.
- 래퍼 시험은 브로커를 `127.0.0.1:9`(`UNREACHABLE`)로 두어 **브로커가 필요 없다**.
- 비차단 위생: `t2_6` 이 `monkeypatch` 대신 `os.environ` 을 직접 바꾼다. `try/finally` 로 복구하므로 새지는 않는다(PR 후속 3).
- `t3_7` 은 reconcile 의 `MAM_JOBS_DIR` 우선을 소스 문자열로만 고정한다(브로커가 필요한 경로라서다). 약하지만 명시되어 있다.
- 격리 실측(실제 `.mam/jobs`, 데몬, 워크트리 상태)은 §7.
## 7. (7) 테스트 실행 결과
| 실행 | 결과 |
|---|---|
| **전체 스위트** @ `4a81d15`(워크트리, 11:52–12:27Z, 오케스트레이터·다른 리뷰어 스위트와 동시) | **937 passed, 4 failed in 2103 s**. HEAD 전후 동일(`4a81d15`) |
| 실패 4건 | pane-busy `test_issue14…::…ignores_pane_busy_token`(rc 0: 늦은 completed 가 수락됨), 벽시계 `…never_outlives_the_wall_clock`(closed after **28.12 s** > 28), AC1(rc 2), AC5(rc 2) |
| 4건 단독 재실행 — PR `4a81d15` | 3 failed / 1 passed(AC1 통과). 이때 공개 브로커가 `no CONNACK from broker` 로 재시도하고 있었다 |
| 4건 단독 재실행 — base `5611859`(본 체크아웃) | 2 failed / 2 passed(AC1·AC5 실패) |
- **판단**:
- 4건은 #18 리뷰에서 main 계열 코드(`135d76f`·`7e1d3f0`·`ab842f1`)로도 같은 방식으로 여러 번 실패했던 T-1 부류다. 벽시계 28.1 s 는 그때와 같은 값이다.
- 같은 브로커 상태에서 base 도 이 부류가 실패한다.
- PR 이 래퍼 submit 에 python 호출 1회(`_logs_dir_abs`)를 추가했지만, 측정된 벽시계 값은 main 과 같다.
- **PR 이 만든 실패는 0건으로 본다.**
- PR 본문의 "33건 환경 실패" 는 이 환경(macOS, venv Python 3.9)에서는 재현되지 않았다. 이 환경의 실패는 위 4건뿐이다.
- **격리**:
- 본 체크아웃은 `main` `5611859` 클린을 유지했다. 실제 `.mam/jobs` 추가는 0 이다.
- 워크트리의 추적 파일은 변하지 않았다(`?? .venv` 만).
- **그러나 무시 대상 `.mam/` 이 워크트리에 생겼다**: `.mam/delegate_job_logs`(잡 21개)와 `.mam/shim`. 생성 시각이 20:52–21:13 KST 로 이 스위트 실행 중이다.
- 일부 시험이 `$REPO_ROOT/.mam` 에 쓴다(#20 위생 문제). PR 후속 4 가 지목한 `test_issue14…::_job_for_inprocess_publish` 의 감사 로그와, 샌드박스 없이 `lib.sh` 를 소싱할 때 생기는 shim 이다.
- PR 이 새로 만든 문제는 아니지만, 워크트리 병렬 운영에서는 워크트리마다 쌓인다.
- 읽기 전용 규칙에 따라 지우지 않았다. 오케스트레이터가 정리할 것을 권한다.
- 샌드박스 heartbeat 타이머: 실행 직후 집계에서 pytest 경로 타이머 3개가 보였으나, 재확인했을 때는 0개였다(동시에 돌던 다른 스위트의 것으로 보인다).
- 모니터 데몬 전후 비교는 스냅샷 필터 오류로 판정하지 못했다.
## 8. (8) #21 작업 항목과 후속
- **완료**: 항목 1(래퍼 경로), 2(러너 옵션·가드), 3(바인딩 영속·전파, status/reconcile/stop/loop 의 `MAM_JOBS_DIR`), 6(문서 정정). 각각 시험으로 고정되어 있다.
- **제외의 타당성**: 4(준비·해체 헬퍼, 데몬 기본화), 5(리뷰 identity·검증 격리), 7(실제 herdr 수용 사례), 8(다중 루프). 모두 설계 결정이나 실환경이 필요한 별도 작업이다. 1단계 운영(구현은 루프 밖, 리뷰 루프 하나)에 필요한 최소 변경만 이 PR 에 들어갔다는 판단에 동의한다.
- **후속 권고 5건은 적절하다**. 추가로 두 가지를 권한다.
- 6) 항목 4 전까지 §3 레시피에 `MAM_NO_SUBSCRIBER_DAEMON=1`(§5).
- 7) 첫 실제 워크트리 creator 전에 항목 7 의 "실제 herdr `--env` 전파" 수용 사례를 먼저 확인한다(현재는 mock 검증뿐). 전파가 안 되더라도 주입 명령이 절대경로라 잡 발행은 동작하지만, creator 가 스스로 부르는 스킬은 워크트리 `.mam` 을 쓰게 된다.
## 9. (9) 병합을 막을 결함
- **알려진 미비**:
- (a) 버전 bump 가 없다(4.5.0 유지). 새 CLI 옵션과 새 모듈이므로 4.6.0(MINOR)과 VERSIONS 항목이 필요하다.
- (b) `docs/design/reviews/review-agy-fd384d6b.md` 의 markdown 줄바꿈용 trailing whitespace 로 `git diff --check` 가 실패한다.
- 둘 다 확인했다. 코드 파일에는 whitespace 문제가 없다.
- **그 밖에 병합을 막을 결함은 찾지 못했다**.
- 비차단:
- create 의 바인딩 해석 실패 삼킴(§3, PR 후속 1)
- §3 레시피의 `MAM_NO_SUBSCRIBER_DAEMON=1`(§5)
- `t2_6` 의 `os.environ` 직접 조작(§6)
- 시험의 `$REPO_ROOT/.mam` 쓰기(§7, #20)
- T-1 시간 임계 시험
## 10. 종합
- PR #22 는 #21 항목 1·2·3·6 을 정확히 구현했다.
- 래퍼 경로는 공백 경로와 다른 cwd 에서도 절대·인용이 맞다(실험).
- 러너는 소스 루트와 제어 루트를 분리하고 diff 수집과 같은 정의의 fail-closed 가드를 둔다.
- 제어 평면 바인딩을 영속·전파하면서도 기존 워크스페이스와 바인딩 없는 행의 동작을 바꾸지 않는다.
- `lib.sh` 변경은 idle pane 재사용 조건에 한정되며 계약 시험은 강화되었다.
- 전체 스위트의 실패 4건은 base 에서도 같은 부류로 실패하는 기존 브로커 시간 임계 시험이다.
- 병합 전에 알려진 미비 (a)(b)를 릴리스 커밋으로 처리하면 된다. 설계 재검토는 필요 없다.
[VERDICT: PASS]