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
161 lines
17 KiB
Markdown
161 lines
17 KiB
Markdown
# 🔍 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 옵트아웃)을 넣어야 한다.
|
|
- ⚠️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=…` 로 감싸야 한다.
|
|
|
|
## 2. §6-Q1 · 브리프 (5) — 두 문제의 해소와 놓친 경로
|
|
|
|
- **체크아웃 전환 사고**: 커밋하는 주체(creator, 오케스트레이터)가 모두 워크트리에서 커밋하므로 **해소된다**.
|
|
- 다만 본 체크아웃에서 누구든 `git checkout` 할 수 있다는 점은 그대로다.
|
|
- 싸고 직교하는 방어로 **본 체크아웃 커밋 가드**를 함께 권한다: `pre-commit` hook 이 `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 에 이 운영 형태를 명시해야 한다.
|
|
- **놓친 실패 경로**:
|
|
1. **리뷰어가 creator 의 살아 있는 워크트리에서 검증**한다(§2-C "그 경로에서 `git diff`·테스트").
|
|
- creator 가 리뷰 중에 커밋·편집하면 검증 대상이 바뀐다. #18·#19 에서 리뷰 중 HEAD 이동이 세 번 있었다.
|
|
- 리뷰어의 테스트 산출물이 creator 트리에 남는다.
|
|
- → 리뷰마다 **detached 워크트리**(`git worktree add --detach "$TMP/review-<job>" <head sha>`, 리뷰 뒤 제거)로 검증 대상을 불변으로 만들 것을 권한다. 새 에이전트 인스턴스가 필요 없는 C 안의 보강이다.
|
|
2. **§3 의 `.venv` symlink 가 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)에 포함한다.
|
|
3. **워크트리 모니터 데몬**(§1 ⚠️1).
|
|
4. **워크트리 삭제 뒤 남는 yaml 행**: herdr 가 죽으면 reconcile 이 drift A 로 `terminated` 표기한다(`reconcile.sh:581-600`). 행은 남지만 대화는 재개할 수 없다. → 해체 순서를 문서화한다: **stop → (재개 불필요 확인) → 워크트리 제거**.
|
|
5. **토픽 지문·HMAC**: 문제없다.
|
|
- 잡 토픽은 레코드의 `topic_prefix`(`python/mqtt/jobs/<id>`, `mqtt_common.py:137`, `:351`)이고, HMAC 은 잡 레코드의 `auth_token` 이다. 레지스트리 경로만 맞으면(§3) 워크트리와 무관하다.
|
|
- 지문은 **모니터 구독**에만 쓰인다(`reconcile.sh:288-291`). 제어 평면이 하나면 지문도 하나다.
|
|
- 문서 §1 표의 해당 행에 이 점을 적어 둘 것.
|
|
6. (부수) 러너는 리포트를 `workspace_root/.agents/reports/…` 로 보관한다(`runner.py:554`, `:770-780`). C 안에서는 본 체크아웃, 즉 `main` 트리로 간다. 이슈 브랜치로 보낼지 정해야 한다.
|
|
|
|
## 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·체크포인트, 리뷰어 대기 큐)를 끝낸 뒤로 미룬다.
|
|
|
|
## 5. §6-Q3 · 브리프 (6) — A/B/C 평가
|
|
|
|
- **동의**:
|
|
- A 의 비용(에이전트 수·토큰 한도가 이슈 수에 비례, 문맥 초기화, 관측 분산) 평가는 타당하다.
|
|
- B 는 creator 가 여전히 본 체크아웃에서 브랜치를 옮기므로 #18 유형이 남는다는 판단이 옳다.
|
|
- C 가 쓰는 쪽만 격리하는 최소 변경이다.
|
|
- **대안·보강 2건**:
|
|
1. **본 체크아웃 커밋 가드**(`pre-commit` hook 또는 커밋 명령 래퍼의 `git branch --show-current` 확인). 문제 2 를 C 와 무관하게 막는 가장 싼 방어다. C 채택과 함께 도입할 것.
|
|
2. **리뷰마다 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: 본 체크아웃 커밋 가드.
|
|
- **§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` 범위, 모니터 데몬 옵트아웃, `.venv` symlink exclude
|
|
- 운영 규약: resume·해체, 리뷰어 detached 워크트리, 커밋 가드
|
|
- 모두 문서 보강과 작은 코드 변경으로 해결되며 설계를 뒤집을 결함은 아니다.
|
|
|
|
[VERDICT: PASS]
|