Review ffb5055e (planner-reviewer-codex-01): - R1 [P1] worktree_remove.sh: the clean-tree and submodule checks ran only before --stop, so work a stopping session wrote (or committed, or stashed) was deleted by the forced removal. The checks are one function now, run before any change and again right before the removal; any finding then refuses with exit 3 (sessions already stopped, worktree kept), and the removal method follows that latest look. - R2 [P1] lib_py/worktree.py: a submodule stash is reachable from no branch and leaves the status clean, and the forced removal deletes the worktree's own submodule repositories. `git stash list` in every submodule is a separate finding (`stashes`), refused like dirty/unpushed; errors fail closed as before. - R3 [P2] worktree_prepare.sh: when one submodule cloned and a later one failed, the rollback deleted modules/ while the cloned checkout's .git file still pointed into it, and `git status` failed in the worktree. The rollback now undoes only this attempt: each submodule whose git dir did not exist before is deinitialised before its git dir is deleted; submodules initialised earlier are kept; shared submodule.* config values are restored exactly (new keys removed, pre-existing values put back - init and deinit both write .git/config). A final `git status` either confirms a usable checkout or prints what to inspect. Tests W6e (a write during stop keeps the worktree and the file), W6f (a submodule stash refuses), W6g (partial failure on a fresh worktree: usable, no modules/, no new config, then prepare and a plain removal work) and W6h (a failing retry keeps the earlier `sm` init and its config value). The skill docs and the design doc describe the recheck, the stash rule and the scoped rollback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
17 KiB
17 KiB
워크트리 운영 모델 제안 — creator 만 워크트리, 리뷰어는 공유 (Design Proposal)
- 상태: 리뷰 반영(4건, docs/design/reviews/) — 1단계 선행 작업 구현(Issue #21 항목 1·2·3·6)
- 작성: 오케스트레이터 세션
dbe6dd10-a210-4b46-b613-58a1860499b6, 2026-09-14 - 배경 이슈: #17 항목 4(셸 계층 축소)·#18 사후 분석(체크아웃 전환 사고)
1. 문제
MAM 은 워크스페이스 = .agents/skills 가 들어 있는 체크아웃 경로 라는 가정 위에 서 있다.
| 경로에서 파생되는 것 | 근거 |
|---|---|
WORKSPACE_ROOT |
lib.sh:26 — ${WORKSPACE_ROOT:-$SKILL_DIR/../..} |
.mam/ (agent-sessions.yaml, jobs 레지스트리, 감사 로그, monitor.lock) |
$WORKSPACE_ROOT/.mam (gitignore 대상) |
| herdr 서버 이름 | resolve_herdr_session — 워크스페이스 경로 슬러그, --herdr-session 으로 override |
| 모니터 토픽 지문 | reconcile.sh:288-290 — sha256(abspath(workspace_root))[:12]. 모니터 구독에만 쓰인다. 잡 토픽은 레코드의 topic_prefix python/mqtt/jobs/<job_id>(mqtt_common.py:137, :351), HMAC 은 레코드의 auth_token 이다 |
| claude 대화 저장 키 | ~/.claude/projects/<cwd 경로 키> — resume 는 같은 디렉터리여야 함 |
그 결과 두 가지 문제가 있다.
- 동시 이슈 작업이 불가능하다. 팀 리더 에이전트 5개와 오케스트레이터가 하나의 체크아웃을 공유하므로 브랜치는 한 번에 하나만 체크아웃된다. 이슈 A 의 리뷰 중 이슈 B 의 구현을 시작할 수 없다.
- 체크아웃 전환 사고가 난다. #18 작업 중 브랜치 생성 1분 뒤 HEAD 가
main으로 되돌아가(reflogmoving from fix/issue-18 to main, 18:32) 이후 12개 커밋이main에 직접 쌓였다. 병합 커밋 없이 push 되어 이력을 되돌리지 않았다. 공유 체크아웃에서는 어떤 프로세스든checkout하나로 다른 모든 참여자의 커밋 위치를 바꿀 수 있다.
2. 후보안
A. 모두 워크트리별 (오케스트레이터 + creator + 리뷰어)
- 장점: 런타임 분리가 가장 강하다(단 git 워크트리는 refs·객체 저장소를 공유하고,
.venv·캐시·자격증명·브로커도 공유 자원이다). 워크트리 준비·수명 처리는 별도로 필요하다. - 단점: 이슈마다 에이전트를 새로 띄운다. 용량 추정: 이슈당 5개 에이전트는 운영 정책이고, 토큰 사용은 살아 있는 세션 수가 아니라 실제 작업량에 비례한다. 리뷰어 대화 문맥이 이슈마다 초기화된다.
.mam·모니터·토픽 지문이 이슈마다 흩어져 관측이 분산된다.
B. 오케스트레이터 커밋만 워크트리, 에이전트는 본 체크아웃
- 장점: 오케스트레이터 커밋은 격리된다.
- 단점: 병렬성이 없다. 에이전트는 대화 하나짜리 프로세스라 잡을 하나씩만 처리하고, 두 루프가 같은 creator·리뷰어를 두고
session_busy(rc 5) 로 경합해 직렬화된다. creator 는 여전히 본 체크아웃에서 브랜치를 옮겨 커밋해야 하므로 #18 사고 유형이 그대로 남는다. 리뷰어가 검증하는 트리가 브랜치 트리가 아니다.
C. (제안) creator 만 워크트리, 리뷰어 풀은 공유, 제어 평면은 하나
- creator = 워크트리 전용 인스턴스. 제품 코드를 편집·커밋하는 유일한 에이전트이므로 cwd 가 곧 이슈 브랜치여야 한다. 오케스트레이터 커밋과 루프의 리포트 승격(
runner.py의 Promotion 블록 → 제어 루트.agents/reports/)은 별도 소유다. 이슈마다creator-<agent>-<issue>로 만들고 끝나면 정지(대화 보존, 필요 시 재개). 재개하려면 같은 워크트리 경로가 있어야 한다(대화 키가 cwd). - 리뷰어 풀 = 본 체크아웃 공유. 리뷰어는 소스를 편집하지 않는다. 그러나 테스트 실행은 쓰기를 동반하므로 '읽기 전용'이 아니다. 검증 격리(head SHA detached 워크트리 또는 격리 옵션)와 (loop_id, base, head) 판정 결속은 항목 5 에서 정한다. 1단계 브리프는 러너가 경로·base·head·커밋 수를 자동 삽입한다(
--worktree/--base). 리뷰가 병목이면 리뷰어 인스턴스를 추가한다. - 오케스트레이터 커밋 = 워크트리. 본 체크아웃은 리뷰어의 작업 공간이므로 오케스트레이터는 그곳에서 브랜치를 바꾸지 않는다.
- 제어 평면 = 본 체크아웃의
.mam하나. create/resume 이 바인딩(WORKSPACE_ROOT·AGENT_SESSIONS_YAML·MAM_JOBS_DIR·DELEGATE_JOB_LOGS_DIR)을 행(control_plane)에 영속하고,--workspace기본값과 다르면 pane 에 명시 전파한다(항목 3). 호출자 env 만으로는 pane 상속이 보장되지 않는다(lib.sh:667-680 — new-session 은 선행K=V토큰만--env로 바꾼다). 행의pane.cwd는 워크트리다.
3. C 안의 동작 방식 (1단계 + 2단계 루프 A 반영)
MAIN=/path/to/multi-agent-mux # 본 체크아웃: 리뷰어 풀 + .mam (제어 루트)
WT=/path/to/multi-agent-mux-wt/issue-20 # 이슈 워크트리 (소스 루트)
# 워크트리 준비 + creator 생성(항목 4, worktree_prepare.sh): git worktree add, 제어 루트 .venv 링크,
# 공통 info/exclude 에 /.venv·.pytest_cache/, creator 는 제어 루트 바인딩과 MAM_NO_SUBSCRIBER_DAEMON=1 로 생성.
# 행의 pane.cwd 는 워크트리, control_plane 은 본 체크아웃. 서브모듈은 --submodules 를 줄 때만 초기화한다.
cd "$MAIN" && bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \
--path "$WT" --branch fix/issue-20 --base main \
--creator-agent claude --session creator-claude-issue20 \
--herdr-session multi-agent-mux --herdr-workspace mam-agent
# 출력 끝의 BASE=<sha> 가 워크트리 HEAD 다. (b) 흐름이면 구현 위임 전에 기록해 둔다.
# (a) 루프가 구현까지 한다: base 는 Phase 2 시작 시점의 워크트리 HEAD
cd "$MAIN" && bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--worktree "$WT" --creator creator-claude-issue20 --all-reviewer --task "Issue #20 ..."
# (b) 구현은 루프 밖(1단계 기본): base 를 구현 위임 **전에** 기록한다(prepare 가 출력한 BASE 와 같다)
BASE=$(git -C "$WT" rev-parse HEAD)
bash "$MAIN/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job" submit --type direct \
--agent claude --agent-session herdr:creator-claude-issue20 --workdir "$MAIN" --prompt "Issue #20 ..."
# ... creator 가 $WT 에서 구현·커밋 ...
cd "$MAIN" && bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \
--worktree "$WT" --base "$BASE" --review-only --creator creator-claude-issue20 --all-reviewer --task "Issue #20 review"
# 정지·재개: --workspace 는 원래 워크트리 경로여야 한다(다른 경로·삭제된 경로는 exit 2).
# 재개는 행에 영속된 바인딩을 다시 전파한다
bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh" --session creator-claude-issue20
bash "$MAIN/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh" \
--workspace "$WT" --agent claude --session creator-claude-issue20
# 정리(worktree_remove.sh): 잠금·세션·잡·청결·서브모듈 판정을 모두 통과해야 정지·제거한다.
# 하나라도 걸리면 exit 3 이고 아무것도 바꾸지 않는다. 브랜치는 남긴다.
bash "$MAIN/.agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh" --path "$WT" --stop
--task에 base 를 적지 말 것. 러너가 넣는 base 와 둘이 된다. 리뷰어가 받는 base 는 러너의[워크트리 컨텍스트]블록 하나뿐이어야 한다.- 모니터(
reconcile.sh)는 본 체크아웃에서만 띄운다. - 브리프·발행 명령(
--registry-dir, 감사 로그 루트DELEGATE_JOB_LOGS_DIR)·Output Report Path 를 제어 평면 절대경로로 바꿨다(항목 1). creator 는 워크트리 cwd 에서 주입된 명령을 그대로 실행하면 된다. - 루프 러너
--worktree가 추가됐다: diff·base·head 만 소스 루트 기준이고, wrapper·레지스트리·체크포인트·승격은 제어 루트다.--worktree/--base가 있으면 빈 변경분(또는 판정 불가)에 리뷰를 요청하지 않는다(fail-closed).--base는 소스 HEAD 의 조상이어야 하고 env 폴백이 없다. - 오케스트레이터 커밋 규칙: 커밋 전
git branch --show-current확인(메모리 규칙), 본 체크아웃에서는main유지.
4. 기대 효과와 한계
- 병렬화 이득은 구현 단계에서 크다: 워크트리마다 creator 가 동시에 작업하고, 전체 스위트도 워크트리마다 동시에 돈다.
- 리뷰 단계는 풀 크기만큼만 병렬이다. 이슈 3개를 동시에 돌려도 리뷰어 하나는 잡을 순서대로 처리한다(래퍼는 busy 세션에 rc 5 → 러너가
MAM_SESSION_BUSY_RETRY_S뒤 1회 재시도 → 그래도 busy 면 실패). 다중 루프 동시 기동에는 리뷰어 큐잉 또는 리뷰어 인스턴스 증설이 전제다. - 같은
.mam의 두 번째 루프는 경고가 아니라 거부다: loop lock 이 exit 1 로 막는다(run_loop.sh의mam_acquire_loop_lock분기). 체크포인트·loop guard 도 파일 하나다(runner.py의checkpoint_file,MAM_LOOP_MARKER). YAML/SQLite 는 flock·BEGIN IMMEDIATE로 트랜잭션 안전(atomic_yaml.py)하므로, 문제는 DB 손상이 아니라 공유 논리 상태와 리뷰어 소유권이다. - 1단계 운영 형태 = 구현 병렬, 리뷰 루프 하나: creator 구현은 루프 밖(delegate-job direct)에서 워크트리마다 병렬로 한다. 리뷰는
run_loop.sh --worktree <WT> --base <구현 전 sha> --review-only로 한 번에 하나씩 한다. base 를 구현 위임 전에 기록하지 않으면 리뷰를 시작할 수 없다(가드가 빈 변경분을 거부한다). busy 처리는 큐가 아니다(1회 재시도). 다중 루프는 항목 8(리뷰어 스케줄러) 이후다. - 준비·해체 헬퍼는 항목 4 로 구현했다(
worktree_prepare.sh,worktree_remove.sh)..mam.env는 복사하지 않는다. creator 에는 제어 루트의 것을MAM_ENV_FILE로 준다..venv는 제어 루트 것의 심링크다. 무시 규칙(/.venv,.pytest_cache/)은 linked worktree 가 공유하는$GIT_COMMON_DIR/info/exclude에 들어간다. 그래서 본 체크아웃에도 적용되며, 둘 다 본 체크아웃에서도 무시해야 하는 산출물이다.- 서브모듈은
--submodules로만 초기화한다. 초기화는 성공·실패와 무관하게 워크트리 git 디렉터리에modules/를 남기고, 그러면 git 이 force 없는worktree remove를 거부한다. prepare 는 실패한 시도가 만든 것만 되돌리고 exit 1 한다. 새 체크아웃은 git 디렉터리보다 먼저 deinit 해서 워크트리가 쓸 수 있는 체크아웃으로 남게 하고, 새submodule.*공유 설정 키는 지운다. 이전 초기화 상태와 기존 설정 값은 보존한다. - 서브모듈을 초기화한 워크트리는 remove 가 수정, 원격에 없는 커밋(
HEAD포함 — 서브모듈은 detached HEAD 다), stash 를 확인한 뒤--force를 한 번만 써서 제거한다. 이 확인은--stop으로 세션을 정지한 뒤 제거 직전에 한 번 더 한다(정지 중에도 쓰기·커밋·stash 가 생길 수 있다). 사람이git worktree remove --force를 직접 쓰지 말 것. 워크트리 전용 서브모듈 저장소와 미푸시 커밋이 함께 사라진다.
5. 필요한 변경 (Issue #21 작업 항목)
| # | 항목 | 상태 |
|---|---|---|
| 1 | 래퍼 주입 문구·시작 명령(--registry-dir, 감사 로그 루트)·Output Report Path 절대경로화, MAM_JOBS_DIR 존중 |
완료(v4.6.0) |
| 2 | 루프 러너 --worktree / --base / --review-only, 빈 변경분 fail-closed 가드, 러너의 MAM_JOBS_DIR 존중 |
완료(v4.6.0) |
| 3 | 제어 평면 바인딩 영속(control_plane)·명시 전파(create/resume), stop·status·reconcile 의 MAM_JOBS_DIR·행 바인딩 존중 |
완료(v4.6.0). 루프 A 에서 바인딩 계산 실패 처리·조기 종료 경로 영속 보강 |
| 4 | 워크트리 준비·해체 헬퍼, resume 을 원래 cwd 로만 허용, create 의 reconcile/watchdog 기동 위치, MAM_NO_SUBSCRIBER_DAEMON(prepare 가 설정). reconcile 의 루트 밖 cwd 재발견 거부는 제한으로 남김 |
루프 A (이 브랜치) |
| 5 | 리뷰 identity: (loop_id, base_sha, head_sha) 판정 기록·무효화, 리뷰어 검증 격리(detached 워크트리) | 대기 |
| 6 | 이 설계 문서 정정 | 완료(v4.6.0), 루프 A 에서 갱신 |
| 7 | 수용 사례 고정(실제 herdr 의 pane env 전파 포함) | 대기 |
| 8 | 다중 루프 네임스페이스·리뷰어 스케줄러 | 보류 |
6. 리뷰어에게 묻는 것
답변: docs/design/reviews/ 4건, 종합은 Issue #21.
- C 안의 역할 분리(쓰는 쪽만 격리, 읽는 쪽 공유)가 두 문제(병렬성, 체크아웃 전환 사고)를 실제로 해소하는가? 놓친 경로가 있는가(예: 리뷰어가 테스트 중 파일을 만드는 경우, 모니터 reconcile 이 워크트리 cwd 행을 어떻게 다루는지).
- 제어 평면 공유(env override)가 현재 코드에서 안전한가? 특히
pane.cwd가 워크트리인 행을 status/monitor/stop/resume 스킬이 올바르게 처리하는가. - A/B 안 대비 C 안의 비용·이득 평가에 동의하는가? 다른 대안이 있는가?
- §5 작업 항목의 누락·과잉. 특히 다중 루프 동시 기동의
.mam경합을 먼저 풀어야 하는지, 아니면 "루프는 하나씩, 구현만 병렬" 로 시작해도 되는지. - #17 항목 4(셸 계층 축소)와의 순서: 이 모델을 먼저 문서·레시피로 굳히고 항목 4 에서 코드로 흡수하는 순서가 맞는가.
7. 루트 정의와 스킬별 호환성
- 제어 루트:
.mam을 가진 본 체크아웃. 레지스트리·감사 로그·세션 yaml·체크포인트·loop guard·리포트 승격이 여기 있다. - 소스 루트: 이슈 워크트리. creator 의 cwd, 루프의 diff·base·head 대상.
- 런타임 코드 루트: 실제로 실행되는
.agents/skills사본. 루프는 제어 루트의 사본을 실행하며, 워크트리의 미검토diff_collect.sh를 실행하지 않는다.
| 스킬 | 제어 평면 바인딩 | 워크트리 cwd 행 | 1단계 이후 남은 제한 (항목) |
|---|---|---|---|
| create | env 에서 결정 → 행 control_plane 에 항상 영속. --workspace 기본값과 다르면 새 pane 의 생성 호출(pane split/workspace create)에 --env 로 전파한다. 전파가 필요하면 idle pane 을 재사용하지 않는다(agent start -- <args> 는 -- 뒤를 에이전트 인자로 넘기므로 env K=V 를 쓸 수 없다). 전파가 필요한데 경로에 공백이 있으면 exit 2. ~/.local/bin wrapper 경로는 기록만 하고 경고 |
pane.cwd = --workspace. 바인딩 계산 실패 시 워크트리면 exit 2, 제어 루트면 경고 |
— (루프 A: 즉시 reconcile·watchdog 은 제어 루트 코드와 WORKSPACE_ROOT 로 기동, prepare 가 MAM_NO_SUBSCRIBER_DAEMON=1) |
| resume | 행 바인딩 우선(호출자 env 와 다르면 stderr 경고) 재전파. 바인딩 없는 기존 행은 호출자 바인딩으로 채움. 이미 살아 있는 세션에도 바인딩을 영속한다. 계산 실패 시 워크트리면 exit 2 | 원래 cwd 로만: 경로 불일치·부재 시 exit 2 | 워크트리 삭제 시 재개 불가(대화 보존, 같은 경로에 다시 만들면 재개) |
| stop | MAM_JOBS_DIR > 행의 mam_jobs_dir > $WORKSPACE_ROOT/.mam/jobs 순으로 잡 조회·발행. 감사 로그도 DELEGATE_JOB_LOGS_DIR > 행의 delegate_job_logs_dir 순 |
행 cwd 로 uuid 캡처·purge. 워크트리 해체는 worktree_remove.sh 가 판정(잠금·세션·잡·청결·서브모듈) 뒤에만 정지·제거, 서브모듈 워크트리는 검증 후 --force 1회 |
— |
| status | MAM_JOBS_DIR·행 mam_jobs_dir·감사 로그(DELEGATE_JOB_LOGS_DIR·행 delegate_job_logs_dir)·기존 후보 순 조회 |
행 cwd 로 대화 키 | — |
| monitor/reconcile | MAM_JOBS_DIR 우선(정적 가드로만 검증) |
기존 행은 행 cwd 로 검증 | 루트 밖 cwd 의 미등록 세션은 자동 등록 안 됨 → create/prepare 로만 등록 (남은 제한) |
| delegate-job | --registry-dir > MAM_JOBS_DIR > .mam/jobs(--workdir 기준), 모두 절대화. 발행 명령이 감사 로그 루트(DELEGATE_JOB_LOGS_DIR)를 고정 |
문구·명령·리포트 경로 절대 | 작은따옴표가 든 경로는 지원 범위 밖 |
| loop | MAM_JOBS_DIR 존중, 래퍼에 --registry-dir 명시. --worktree 로 소스 루트 분리 |
diff·base·head 만 소스 루트. 구현이 루프 밖이면 --base <구현 전 sha> --review-only 필수. --worktree/--base 가 있으면 빈 변경분 거부 |
루프 하나(8), 리뷰 identity·검증 격리(5), 레거시 셸 루프(MAM_LOOP_PY=0)는 새 옵션 거부 |
실제 herdr 가 pane split --env / workspace create --env 로 pane 환경을 넘기는지는 1단계에서 mock 수준으로만 검증했다. 실제 전파는 항목 7 의 수용 사례로 고정한다.