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
17 KiB
17 KiB
🔍 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(detached4a81d15) git log 5611859..HEAD9커밋,git diff 5611859...HEAD19파일 +1905/−78- 설계 커밋
fe96db1·dcf2fab과 구현·교정 커밋 7개(d3d3a76→4a81d15)
- 워크트리
- 방법:
- 워크트리 diff 를 정독하고 이슈 #21·PR 본문과 대조했다.
- 임시 디렉터리에서 실험 1건을 했다(공백 경로 명령 실행).
- 워크트리에서 전체 스위트를 읽기 전용 조건(
PYTHONDONTWRITEBYTECODE=1 -p no:cacheprovider)으로 실행했다. 실패는 base(본 체크아웃5611859)에서 같은 시험으로 대조했다. - 코드는 수정하지 않았다.
- 안전 규칙 준수:
- 본 체크아웃은
main5611859를 유지했다(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.shdelegate 블록)
- 브리프 경로:
- 실험 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_WORKTREEenv 는 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).
- 소스 루트 기준: diff(제어 루트의
- 빈 변경분 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.shnew-session 이 이것을--env로 바꾸는 기존 경로를 쓴다. 13259ed/af8a54c: env 플래그가 있으면 idle pane 을 재사용하지 않고pane split/workspace create호출에--env를 싣는다(lib.sh775-787 부근).- 재사용 pane 에
env K=V를agent start --뒤 에이전트 인자로 넘기던 종전 결함(R1)이 없어졌다. - 공백이 든 바인딩을 전파해야 하면 rc 3 → create·resume 이 exit 2 로 거부한다(
lib.sh가--env플래그를 인용 없이 나누므로).
- create·resume 은
- 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 는 없다.
- new-session 의 idle pane 재사용 조건에
- 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건으로 본다.
- 4건은 #18 리뷰에서 main 계열 코드(
- PR 본문의 "33건 환경 실패" 는 이 환경(macOS, venv Python 3.9)에서는 재현되지 않았다. 이 환경의 실패는 위 4건뿐이다.
- 격리:
- 본 체크아웃은
main5611859클린을 유지했다. 실제.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건은 적절하다. 추가로 두 가지를 권한다.
-
- 항목 4 전까지 §3 레시피에
MAM_NO_SUBSCRIBER_DAEMON=1(§5).
- 항목 4 전까지 §3 레시피에
-
- 첫 실제 워크트리 creator 전에 항목 7 의 "실제 herdr
--env전파" 수용 사례를 먼저 확인한다(현재는 mock 검증뿐). 전파가 안 되더라도 주입 명령이 절대경로라 잡 발행은 동작하지만, creator 가 스스로 부르는 스킬은 워크트리.mam을 쓰게 된다.
- 첫 실제 워크트리 creator 전에 항목 7 의 "실제 herdr
-
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]