claude, agy, grok: PASS with amendments; codex: NOT PASS (recipe overstates current env-override support; review identity and lifecycle ownership underspecified). Direction (creator per worktree, shared reviewer pool) agreed by all four. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
17 KiB
17 KiB
🔍 Design Review — Job e89f62d5 (워크트리 운영 모델 제안, design/worktree-model @ fe96db1)
- Reviewer:
planner-reviewer-claude-01(role: planner,reviewer) · 2026-09-14 - 대상:
docs/design/worktree-creator-model.md(워크트리multi-agent-mux-wt/design-worktree-model, 커밋fe96db1, 1파일 +86).- 본 체크아웃은 이 잡 내내
main@5611859(v4.5.0 코드)에 있다. - 아래의 코드 근거(파일:라인)는 모두 이
main기준이다.
- 본 체크아웃은 이 잡 내내
- 방법:
- 문서를 정독했다.
- 문서가 기대는 코드 경로를 create·status·monitor·stop·resume·delegate-job·loop 러너 순으로 추적했다.
- scratchpad 임시 git 저장소에서 실험 1건을 했다(gitignore 와 symlink).
- 문서·코드는 수정하지 않았다. 체크아웃 전환, 워크트리 조작, 실제
.mam·세션 조작도 없었다.
0. 요약
| 항목 | 판정 | 핵심 |
|---|---|---|
| §6-Q1 두 문제 해소 | 보완 | 커밋 격리는 해소된다. 병렬성은 "루프 밖 creator 병렬 + 리뷰 루프 1개" 로만 가능하다(루프 lock 이 두 번째 루프를 거부). 놓친 경로 5건(§2) |
| §6-Q2 / 브리프 (1) 제어 평면 env override | 보완 | status·monitor·stop 은 행의 pane.cwd 를 존중해 안전하다. 그러나 resume 은 --workspace $WT 가 필수이고, 모니터 데몬이 워크트리 코드로 뜰 수 있다 |
| 브리프 (2) 주입 문구 상대경로 | 동의 + 범위 확대 | brief 경로뿐 아니라 --registry-dir .mam/jobs 와 Output Report Path 도 상대경로다. 이대로면 creator 의 started 가 실패한다(rc 1 agent_blocked). §5-1 은 첫 사용 전 필수다 |
브리프 (3) 러너 diff·base_commit |
동의 + 구체화 | runner.py:397-411, :564-565 는 모두 workspace_root 기준이다. --worktree 는 diff·base 만 바꾸고 wrapper·레지스트리·체크포인트는 본 체크아웃에 두어야 한다 |
브리프 (4) .mam 경합·§5-4 |
보완 | 문서의 "경고" 가 아니라 거부다(run_loop.sh:189-203). 1단계는 §5-4 없이 시작할 수 있다 |
| 브리프 (5) 놓친 실패 경로 | 보완 | .venv symlink 가 무시되지 않음(실험), .pytest_cache 미무시, 리뷰어가 creator 의 살아 있는 트리에서 검증, 워크트리 모니터 데몬, 워크트리 삭제 뒤 resume 불가 |
| §6-Q3 / 브리프 (6) A/B/C 평가 | 동의 + 대안 2건 | C 가 맞다. 추가로 ① 본 체크아웃 커밋 가드, ② 리뷰마다 detached 워크트리 |
| §6-Q4 §5 누락·과잉 | 보완 | §5-1·§5-2·§5-3 범위를 넓히고 3건을 추가한다. §5-4 는 보류해도 된다 |
| §6-Q5 / 브리프 (7) #17 항목 4 순서 | 조건부 동의 | 레시피·문서 먼저. 단 §5-1·§5-2 두 코드 변경은 지금 한다 |
결론: 근본 결함은 없고 C 안은 채택 가능하다. 다만 §7 의 문서 수정과 §5 보강이 첫 적용 전에 필요하다.
1. 브리프 (1) · §6-Q2 — 제어 평면 env override 가 스킬마다 존중되는가
| 구성요소 | override 존중 | 워크트리 pane.cwd 행 처리 |
근거 |
|---|---|---|---|
lib.sh |
WORKSPACE_ROOT(:26)와 AGENT_SESSIONS_YAML(:34)을 env 우선으로 쓴다 |
— | lib.sh:26, :34 |
| create | yaml·레지스트리 ✅(create_session.sh:353, :372-386) |
pane cwd = --workspace(:219, :223, :229)이고 행에 기록된다(:309, :428, :494). herdr 서버는 --herdr-session 을 주지 않으면 워크트리 슬러그가 된다(:163-172) |
아래 ⚠️1 |
| status | yaml ✅(status.sh:22) |
claude 대화 키를 행의 cwd 로 계산한다 ✅(:59-76) |
안전 |
| monitor (reconcile) | yaml·WORKSPACE_ROOT ✅ |
herdr 서버를 행마다 모아 조회한다(reconcile.sh:491-500). 다른 서버의 워크트리 행도 관측되므로 거짓 drift A 가 없다. drift B 자동 등록은 본 체크아웃 아래 pane 만 한다(:651-653). C0 검증은 행 cwd 를 쓴다(:727-730, verify_session.py:90-93) |
안전 |
| stop | yaml ✅, purge 마커는 본 체크아웃 .mam(stop_session.sh:103) |
uuid 캡처·purge 가 행 cwd 를 쓴다(:133-143, :189). herdr 소켓은 행의 herdr_session 을 먼저 쓴다(lib.sh:1484-1490) |
안전 |
| resume | yaml ✅ | --workspace 가 필수다(resume_session.sh:42). 행을 cwd == --workspace 로 찾는다(workspace_uuid.py:58). claude 키는 워크트리 경로다(verify_session.py:75-81) |
⚠️2 |
| delegate-job 래퍼 | .mam.env 는 REPO_ROOT(= 본 체크아웃, :20-24) ✅. MAM_JOBS_DIR 은 참조하지 않는다. REGISTRY_DIR_DEFAULT=".mam/jobs"(:71) |
주입 문구·브리프가 상대경로다 | §3 에서 다룬다 |
| loop 러너 | workspace_root = MAM_REAL_ROOT/WORKSPACE_ROOT/cwd(runner.py:58) |
— | §4 에서 다룬다 |
- ⚠️1 워크트리 모니터 데몬:
- create 는
start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE"(create_session.sh:527)를 부른다. - 그러면 데몬이
<WT>/.agents/skills/…/reconcile.sh(daemon.py:35-36,:63-85)로 뜬다. 중복 판정이 스크립트 경로 기준(:39-55)이라 본 체크아웃 데몬을 보지 못한다. WORKSPACE_ROOT=$MAIN이 상속되면, 이 데몬은 본 체크아웃의monitor.lock을 얻지 못해 곧 끝난다.- 그러나 본 체크아웃 데몬이 없던 순간이면 이 데몬이 워크스페이스 모니터가 된다. 그러면 제어 평면이 이슈 브랜치의 미검토 코드로 돈다.
- §3 레시피에
MAM_NO_SUBSCRIBER_DAEMON=1(#19 옵트아웃)을 넣어야 한다.
- create 는
- ⚠️2 resume:
--workspace $WT로 불러야 워크트리 행과 대화를 찾는다.$MAIN을 주면 행을 찾지 못하거나 pane 이 본 체크아웃 cwd 로 떠 대화 키가 어긋난다.- 워크트리를 지우면 대화를 재개할 수 없다(키가 경로다). 같은 경로로 워크트리를 다시 만들어야 한다.
- 문서 §2-C 의 "끝나면 정지(대화 보존, 필요 시 재개)" 에 "재개하려면 워크트리를 유지하거나 같은 경로로 재생성" 을 덧붙여야 한다.
- pane 런타임 env (확인 권고):
- create 는 herdr 에
CMD_FULL만 넘긴다(create_session.sh:219). create 호출자가 export 한 override 가 에이전트 프로세스의 env 에 남는지는 herdr 서버 동작에 달렸다. - 남지 않으면 creator 가 워크트리에서
bash .agents/skills/…로 부르는 스킬은 워크트리의.mam을 제어 평면으로 쓴다(lib.sh:26). - 브리프에 "워크트리에서 제어 평면 스킬을 실행하지 말 것" 을 넣거나,
CMD_FULL을env WORKSPACE_ROOT=… MAM_JOBS_DIR=…로 감싸야 한다.
- create 는 herdr 에
2. §6-Q1 · 브리프 (5) — 두 문제의 해소와 놓친 경로
- 체크아웃 전환 사고: 커밋하는 주체(creator, 오케스트레이터)가 모두 워크트리에서 커밋하므로 해소된다.
- 다만 본 체크아웃에서 누구든
git checkout할 수 있다는 점은 그대로다. - 싸고 직교하는 방어로 본 체크아웃 커밋 가드를 함께 권한다:
pre-commithook 이git branch --show-current를 확인해 거부한다.
- 다만 본 체크아웃에서 누구든
- 병렬성: 루프 lock 이 같은
.mam에서 두 번째 루프를 exit 1 로 거부한다(run_loop.sh:144,:189-203, loop SKILL.md:235).- 따라서 C 안의 1단계 병렬은 다음 형태뿐이다: creator 구현은 루프 밖(delegate-job direct 나 대화)에서 병렬로, 리뷰 루프는 한 번에 하나.
- §4 와 §6-Q4 에 이 운영 형태를 명시해야 한다.
- 놓친 실패 경로:
- 리뷰어가 creator 의 살아 있는 워크트리에서 검증한다(§2-C "그 경로에서
git diff·테스트").- creator 가 리뷰 중에 커밋·편집하면 검증 대상이 바뀐다. #18·#19 에서 리뷰 중 HEAD 이동이 세 번 있었다.
- 리뷰어의 테스트 산출물이 creator 트리에 남는다.
- → 리뷰마다 detached 워크트리(
git worktree add --detach "$TMP/review-<job>" <head sha>, 리뷰 뒤 제거)로 검증 대상을 불변으로 만들 것을 권한다. 새 에이전트 인스턴스가 필요 없는 C 안의 보강이다.
- §3 의
.venvsymlink 가 git 에서 무시되지 않는다. 실험(임시 저장소,.gitignore에.venv/): symlink.venv는?? .venv로 보인다. 디렉터리 전용 패턴은 symlink 에 맞지 않는다.- 그래서 creator 의
git add -A가 symlink 를 커밋한다. - 또 저장소
.gitignore에.pytest_cache가 없어서(.gitignore:14-25에.mam/·.venv/·__pycache__/·.mam.env*만 있음),-p no:cacheprovider없이 돌린 pytest 도 산출물을 남긴다. - → 워크트리의
.git/info/exclude에/.venv·.pytest_cache/를 넣는 것을 준비 헬퍼(§5-3)에 포함한다.
- 그래서 creator 의
- 워크트리 모니터 데몬(§1 ⚠️1).
- 워크트리 삭제 뒤 남는 yaml 행: herdr 가 죽으면 reconcile 이 drift A 로
terminated표기한다(reconcile.sh:581-600). 행은 남지만 대화는 재개할 수 없다. → 해체 순서를 문서화한다: stop → (재개 불필요 확인) → 워크트리 제거. - 토픽 지문·HMAC: 문제없다.
- 잡 토픽은 레코드의
topic_prefix(python/mqtt/jobs/<id>,mqtt_common.py:137,:351)이고, HMAC 은 잡 레코드의auth_token이다. 레지스트리 경로만 맞으면(§3) 워크트리와 무관하다. - 지문은 모니터 구독에만 쓰인다(
reconcile.sh:288-291). 제어 평면이 하나면 지문도 하나다. - 문서 §1 표의 해당 행에 이 점을 적어 둘 것.
- 잡 토픽은 레코드의
- (부수) 러너는 리포트를
workspace_root/.agents/reports/…로 보관한다(runner.py:554,:770-780). C 안에서는 본 체크아웃, 즉main트리로 간다. 이슈 브랜치로 보낼지 정해야 한다.
- 리뷰어가 creator 의 살아 있는 워크트리에서 검증한다(§2-C "그 경로에서
3. 브리프 (2) — 주입 문구의 상대경로 (§5-1)
- 동의, 그리고 범위가 더 넓다. 래퍼가 에이전트에 주는 세 경로가 모두 cwd 상대다.
| 문자열 | 위치 | 워크트리 cwd creator 에게 |
|---|---|---|
브리프 .mam/jobs/$JOB_ID/brief.md |
래퍼 :199, :455 |
없음(문서가 짚은 부분) |
발행 명령 --registry-dir $REGISTRY_DIR(기본 .mam/jobs, :71) |
:198, :454 |
$WT/.mam/jobs 로 해석된다 → job record not found, rc 1. heartbeat 타이머도 상대 레지스트리·cwd 로 뜬다. started 가 안 나가므로 T 뒤 agent_blocked 로 닫힌다 |
Output Report Path .mam/jobs/$JOB_ID/…/report-final.md |
:178, :352 |
리포트가 $WT/.mam/… 에 쓰인다. 러너의 find_report(runner.py:389-395, 본 체크아웃 .mam)가 찾지 못한다 |
- 래퍼가
MAM_JOBS_DIR을 보지 않으므로 env override 로는 풀리지 않는다. - → §5-1 을 "
REGISTRY_DIR을 절대경로로 정규화하고, 브리프·발행 명령·리포트 경로 세 곳 모두 절대경로로" 로 고치고, 첫 C 안 적용 전 필수로 표시한다.
4. 브리프 (3)·(4) — 러너 diff·base_commit, .mam 경합
- (3) 동의:
collect_changes_diff는workspace_root의 스크립트와 cwd 를 쓴다(runner.py:397-411).base_commit은workspace_root의HEAD다(:564-565). 본 체크아웃이main이면 diff 는 비고 base 는main이다. 이번 #18 브리프들의 "(no changes since base commit)" 과 같은 증상이다.- §5-2 의
--worktree는 다음을 정확히 지정해야 한다: 바뀌는 것은 diff 수집 경로,base_commit(워크트리에서git merge-base main HEAD), 브리프에 넣는 경로·base·head 뿐이다. 그대로 두는 것은 wrapper 경로(:288), 레지스트리, 체크포인트(:60), 리포트 보관이며 본 체크아웃의 것을 쓴다.
- (4) 보완:
- 문서 §4 는 "동시 루프 금지" 를 경고로 서술하지만, 실제로는 lock 이 거부한다(
run_loop.sh:189-203). 체크포인트도 파일 하나다(runner.py:60). - 세션 DB 는 SQLite 읽기이고
timeout=10(runner.py:199-203)이다. 쓰기 경합은 yaml 의atomic_dump_yaml(flock) 쪽이다. - 리뷰어 busy 는 rc 5 →
MAM_SESSION_BUSY_RETRY_S(기본 30) 뒤 1회 재시도(runner.py:312-322) → 실패로 이어진다. 루프 둘이 같은 리뷰어를 두고 다투면 한쪽이 실패한다. - 결론: 1단계를 "리뷰 루프는 하나씩, creator 구현만 병렬" 로 시작하면 §5-4 는 필요 없다. 루프 여러 개를 동시에 돌리는 것은 §5-4(루프 id 별 marker·체크포인트, 리뷰어 대기 큐)를 끝낸 뒤로 미룬다.
- 문서 §4 는 "동시 루프 금지" 를 경고로 서술하지만, 실제로는 lock 이 거부한다(
5. §6-Q3 · 브리프 (6) — A/B/C 평가
- 동의:
- A 의 비용(에이전트 수·토큰 한도가 이슈 수에 비례, 문맥 초기화, 관측 분산) 평가는 타당하다.
- B 는 creator 가 여전히 본 체크아웃에서 브랜치를 옮기므로 #18 유형이 남는다는 판단이 옳다.
- C 가 쓰는 쪽만 격리하는 최소 변경이다.
- 대안·보강 2건:
- 본 체크아웃 커밋 가드(
pre-commithook 또는 커밋 명령 래퍼의git branch --show-current확인). 문제 2 를 C 와 무관하게 막는 가장 싼 방어다. C 채택과 함께 도입할 것. - 리뷰마다 detached 워크트리(§2-1). 리뷰어 풀은 공유하되 검증 대상은 불변으로 만든다. A 의 격리 이점을 에이전트 증설 없이 얻는다.
- 본 체크아웃 커밋 가드(
6. §6-Q4 · §6-Q5 — §5 작업 항목과 #17 항목 4 순서
- §5 수정 제안:
- §5-1: 범위 확대(§3). 첫 적용 전 필수.
- §5-2:
--worktree의 영향 범위 명시(§4). 첫 적용 전 필수. - §5-3: 준비 헬퍼에 다음을 포함한다.
MAM_NO_SUBSCRIBER_DAEMON=1.git/info/exclude(/.venv,.pytest_cache/)--herdr-session <본 서버>고정.mam.env는 워크트리 스크립트를 쓸 때만 필요(래퍼·mqtt_common은 스크립트 위치 기준으로 찾는다: 래퍼:20-24,mqtt_common.py:54-65)
- §5-4: 보류해도 된다(§4).
- 추가:
- §5-6: 워크트리 creator 의 stop·resume 레시피와 해체 순서(resume 은
--workspace $WT, 삭제 전 stop). - §5-7: 리뷰어 detached 워크트리 규약.
- §5-8: 본 체크아웃 커밋 가드.
- §5-6: 워크트리 creator 의 stop·resume 레시피와 해체 순서(resume 은
- §6-Q5 (조건부 동의):
- 이 모델을 문서·레시피로 먼저 굳히고 #17 항목 4(셸 계층 축소)에서 코드로 흡수하는 순서가 맞다.
- 단 §5-1(래퍼 경로)과 §5-2(러너 옵션)는 작은 변경이고 없으면 C 안이 동작하지 않으므로 지금 한다. 항목 4 가 래퍼를
lib_py로 옮길 때 그대로 따라가면 된다. - 헬퍼(§5-3)와 큐잉(§5-4)은 항목 4 안에서 구현하는 편이 중복을 줄인다.
7. 문서 수정 지점 (절·문장)
- §1 표 "모니터 토픽 지문" 행: "(모니터 구독에만 쓰인다. 잡 토픽·HMAC 은 잡 레코드의
topic_prefix·auth_token)" 을 덧붙인다. - §2-C "creator = 워크트리 전용 인스턴스 … 끝나면 정지(대화 보존, 필요 시 재개)" 에 "재개는 같은 워크트리 경로가 있어야 한다" 를 추가한다.
- §2-C "제어 평면 = …": 모니터 데몬 옵트아웃과, pane 런타임 env 에 override 가 남지 않을 수 있다는 점을 추가한다.
- §2-C "리뷰어 풀 = …": 리뷰어는 creator 의 워크트리가 아니라 head sha 의 detached 워크트리에서 검증한다고 고친다.
- §3 레시피:
MAM_NO_SUBSCRIBER_DAEMON=1을 추가한다.ln -s .venv옆에.git/info/exclude처리를 적는다.- stop·resume 명령 예시를 추가한다.
- "주입 문구의 상대경로 …" 문장을 발행 명령의
--registry-dir와 Output Report Path 까지 넓힌다.
- §4 세 번째 항목: "경고" 를 "lock 이 두 번째 루프를 거부(
run_loop.sh:189-203)" 로 고친다. 1단계 운영 형태(구현 병렬, 리뷰 루프 1개)를 명시한다. - §5: 1·2·3 을 수정하고 6·7·8 을 추가하며, 4 는 "보류 가능" 으로 표시한다(§6).
8. 종합
- C 안(쓰는 쪽만 워크트리, 읽는 쪽 공유, 제어 평면 하나)은 두 문제에 대한 올바른 방향이다.
- status·monitor·stop 이 행의
pane.cwd와 행별 herdr 서버를 이미 존중하므로 현재 코드와도 대체로 맞는다. - 보강이 필요한 곳:
- 첫 적용 전 필수: 래퍼 경로 3종의 절대경로화, 러너
--worktree범위, 모니터 데몬 옵트아웃,.venvsymlink exclude - 운영 규약: resume·해체, 리뷰어 detached 워크트리, 커밋 가드
- 첫 적용 전 필수: 래퍼 경로 3종의 절대경로화, 러너
- 모두 문서 보강과 작은 코드 변경으로 해결되며 설계를 뒤집을 결함은 아니다.
[VERDICT: PASS]