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
192 lines
17 KiB
Markdown
192 lines
17 KiB
Markdown
# 🔍 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]
|