- design doc §3: the hand-assembled worktree/.venv/create recipe is replaced by worktree_prepare.sh --creator-agent (its BASE= line is the base to record); cleanup is worktree_remove.sh --stop; resume needs the original path. - §4: what the helpers do - no .mam.env copy, the shared info/exclude, opt-in submodules (any init leaves modules/, which blocks a plain remove), removal of submodule worktrees with one --force after the checks, and never by hand. - §5: item 4 is loop A on this branch; phase-1 items are done in v4.6.0. - §7: create (control-root reconcile/watchdog, loud binding failure), resume (original cwd only, binding persisted when already running), stop (the removal helper's checks), monitor (register via create/prepare only). - create / stop / resume SKILL.md: usage and exit codes of worktree_prepare.sh and worktree_remove.sh, the original-cwd rule and its exit code (1 -> 2). Frontmatter versions are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
135 lines
17 KiB
Markdown
135 lines
17 KiB
Markdown
# 워크트리 운영 모델 제안 — 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).
|
|
- **리뷰어 풀 = 본 체크아웃 공유.** 리뷰어는 소스를 편집하지 않는다. 그러나 테스트 실행은 쓰기를 동반하므로 '읽기 전용'이 아니다. 검증 격리(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 반영)
|
|
|
|
```bash
|
|
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 는 실패한 초기화의 흔적(`modules/`, 새 `submodule.*` 공유 설정)을 되돌리고 exit 1 한다.
|
|
- 서브모듈을 초기화한 워크트리는 remove 가 수정과 원격에 없는 커밋(`HEAD` 포함 — 서브모듈은 detached HEAD 다)을 확인한 뒤 `--force` 를 한 번만 써서 제거한다. **사람이 `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.
|
|
|
|
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·검증 격리(5), 레거시 셸 루프(`MAM_LOOP_PY=0`)는 새 옵션 거부 |
|
|
|
|
실제 herdr 가 `pane split --env` / `workspace create --env` 로 pane 환경을 넘기는지는 1단계에서 mock 수준으로만 검증했다. 실제 전파는 항목 7 의 수용 사례로 고정한다.
|