Files
multi-agent-mux/docs/design/worktree-creator-model.md
T
GodopuandClaude Fable 5.1 08db1da9d5 fix(issue-21): PR #29 review R1-R3 - untracked-safe cleanliness checks; a worktree gets its own venv
R1/R2: `git status --porcelain` honours status.showUntrackedFiles, so a
`no` setting in a submodule (or the worktree) hid untracked files from the
checks that justify worktree_remove.sh's single --force and from the review
snapshot guard (_dirty_submodules), which then approved content it never
captured. All three checks (lib_py/worktree.py, worktree_remove.sh,
runner._dirty_submodules) now pass --untracked-files=all. The source tree's
own snapshot was already safe: `git add -A` ignores that setting.

R3: worktree_prepare.sh no longer links .venv to the control root's. It
creates the worktree's own venv with the control root's interpreter and
installs requirements*.txt; a leftover link is refused (exit 1), a control
root without an interpreter is a warning (VENV=skipped); VENV= is printed.

Tests: w1 (real venv, kept on re-run), w1b (link refused / skipped), w6e
parametrised over the hidden setting for the post-stop recheck, w6e2
(hidden untracked file in the submodule and in the worktree blocks the
removal), l17 untracked-hidden / during-hidden (guard refuses before the
first review; a mid-round hidden file makes the round stale). Each new
case fails without the fix.

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

20 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 는 같은 디렉터리여야 함

그 결과 두 가지 문제가 있다.

  1. 동시 이슈 작업이 불가능하다. 팀 리더 에이전트 5개와 오케스트레이터가 하나의 체크아웃을 공유하므로 브랜치는 한 번에 하나만 체크아웃된다. 이슈 A 의 리뷰 중 이슈 B 의 구현을 시작할 수 없다.
  2. 체크아웃 전환 사고가 난다. #18 작업 중 브랜치 생성 1분 뒤 HEAD 가 main 으로 되돌아가(reflog moving 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).
  • 리뷰어 풀 = 본 체크아웃 공유. 리뷰어는 소스를 편집하지 않는다. 그러나 테스트 실행은 쓰기를 동반하므로 '읽기 전용'이 아니다. --worktree/--base 에서는 러너가 매 라운드를 스냅샷 (loop_id, base, head, tree) 에 묶고(항목 5), 리뷰어는 그 스냅샷의 detached 리뷰 워크트리(기본) 또는 env 격리 지침으로 소스 루트에서 검증한다. 리뷰 중 소스가 바뀐 라운드의 승인은 버리고 다시 리뷰하며, 승격 직전에도 다시 확인한다. 원격 리뷰어는 리뷰 트리에 닿을 수 없어 기본 거부, --remote-review diff-only 로만 받는다. 리뷰가 병목이면 리뷰어 인스턴스를 추가한다.
  • 오케스트레이터 커밋 = 워크트리. 본 체크아웃은 리뷰어의 작업 공간이므로 오케스트레이터는 그곳에서 브랜치를 바꾸지 않는다.
  • 제어 평면 = 본 체크아웃의 .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/bin/python 으로 만들고 requirements*.txt 를 설치한다. 심링크는 만들지 않으며 남아 있으면 prepare 가 거부한다(링크는 제어 루트 환경을 그대로 가리키고, 워크트리의 git add -A 가 링크 자체를 커밋한 적이 있다). 무시 규칙(/.venv, .pytest_cache/)은 linked worktree 가 공유하는 $GIT_COMMON_DIR/info/exclude 에 들어간다. 그래서 본 체크아웃에도 적용되며, 둘 다 본 체크아웃에서도 무시해야 하는 산출물이다.
    • 서브모듈은 --submodules 로만 초기화한다. 초기화는 성공·실패와 무관하게 워크트리 git 디렉터리에 modules/ 를 남기고, 그러면 git 이 force 없는 worktree remove 를 거부한다. 초기화하지 않은 서브모듈 경로에 이미 파일이 있으면 아무것도 바꾸지 않고 exit 3 으로 거부한다. prepare 는 실패한 시도가 만든 것만 되돌리고 exit 1 한다. 새 체크아웃은 git 디렉터리보다 먼저 deinit 해서 워크트리가 쓸 수 있는 체크아웃으로 남게 하고, 새 submodule.* 공유 설정 키는 지운다. 이전 초기화 상태와 기존 설정 값은 보존한다.
    • 서브모듈을 초기화한 워크트리는 remove 가 수정(untracked 포함, --untracked-files=all 이라 status.showUntrackedFiles 설정으로 숨길 수 없다), HEAD 와 모든 로컬 ref(tag·notes 포함)에서 도달하는, 원격에 없는 커밋(서브모듈은 detached HEAD 다), stash 를 확인한 뒤 --force 를 한 번만 써서 제거한다. 이 확인은 초기화된 체크아웃뿐 아니라 워크트리 modules/ 아래의 모든 저장소(deinit 된 서브모듈, 중첩 서브모듈 포함)에 직접 하며, 확인할 수 없는 저장소가 있으면 거부한다. 이 확인은 --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 워크트리) 루프 B (이 브랜치): 스냅샷 identity·스냅샷 diff·stale 재리뷰·승격 직전 재확인·identity.json(evidence), 원격 리뷰어 거부/diff-only. 리뷰 후속: dirty 서브모듈 거부(스냅샷·stale 검사), identity 모드 셀프 리뷰도 리뷰어 경로로 기록·승격, 리뷰 프롬프트의 검증 대상 단일화
6 이 설계 문서 정정 완료(v4.6.0), 루프 A 에서 갱신
7 수용 사례 고정(실제 herdr 의 pane env 전파 포함) 루프 B (이 브랜치): 수용 사례 8건 테스트 고정(tests/test_issue21_acceptance.py, A1~A8). pane env 전파는 mock herdr 의 --env 호출 단언까지이며, 실제 herdr pane 안의 값 확인과 시험 운영은 단계 C
8 다중 루프 네임스페이스·리뷰어 스케줄러 보류

6. 리뷰어에게 묻는 것

답변: docs/design/reviews/ 4건, 종합은 Issue #21.

  1. C 안의 역할 분리(쓰는 쪽만 격리, 읽는 쪽 공유)가 두 문제(병렬성, 체크아웃 전환 사고)를 실제로 해소하는가? 놓친 경로가 있는가(예: 리뷰어가 테스트 중 파일을 만드는 경우, 모니터 reconcile 이 워크트리 cwd 행을 어떻게 다루는지).
  2. 제어 평면 공유(env override)가 현재 코드에서 안전한가? 특히 pane.cwd 가 워크트리인 행을 status/monitor/stop/resume 스킬이 올바르게 처리하는가.
  3. A/B 안 대비 C 안의 비용·이득 평가에 동의하는가? 다른 대안이 있는가?
  4. §5 작업 항목의 누락·과잉. 특히 다중 루프 동시 기동의 .mam 경합을 먼저 풀어야 하는지, 아니면 "루프는 하나씩, 구현만 병렬" 로 시작해도 되는지.
  5. #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 모드에서 거부(기본) 또는 diff-only(--remote-review diff-only); 원격 리뷰어가 트리를 검증하게 하는 스냅샷 공유(공유 원격의 refs/mam/review/<loop_id>/<n>)는 범위 밖. 레거시 셸 루프(MAM_LOOP_PY=0)는 새 옵션 거부

실제 herdr 가 pane split --env / workspace create --env 로 pane 환경을 넘기는지는 mock 수준으로만 검증했다. 수용 사례 A2 는 러너가 바인딩 4개를 --env 로 싣고 agent start -- 뒤에는 싣지 않음을 고정한다. 실제 herdr pane 안에서 값이 보이는지는 단계 C 시험 운영에서 확인한다.

호환성 메모 (루프 B): 1단계부터 있던 --worktree/--base + 원격 리뷰어 조합은 이전에는 원격 리뷰어가 도달할 수 없는 로컬 경로를 프롬프트에 담아 진행했다. 이제는 어떤 잡도 위임하기 전에 기본으로 거부하며, --remote-review diff-only 로 opt-in 할 수 있다. 옵션 없는 루프는 그대로다. 스냅샷 커밋은 ref 가 없어 gc 가 정리하고(기본 2주), 리뷰 워크트리는 라운드마다 지운다.