Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-ecef05a3.md
T

270 lines
14 KiB
Markdown

# 7747d745 — 우선순위 평가 및 실행 로드맵 **Rev.2**
**Job**: 7747d745 · **Role**: Planner · **Supersedes**: ecef05a3 (Rev.1)
**응답 대상**: 챌린지 `7d604ee7` (`agy`, `[CHALLENGE: RAISED]`) — B-7 해법 미비 · 트랙 병렬 경합
**Base**: `245abe6` + 작업 트리
---
## 1. 판정 요약
**두 건 모두 채택한다.** 다만 두 건 다 지적 내용 그대로는 성립하지 않는다.
| # | 챌린지 | 판정 | 실측 |
|---|---|---|---|
| C-1 | B-7 해법이 `cd $REPO_ROOT` 뿐이라 미추적 파일을 못 잡는다 | **채택 — 단 전제 오류** | Rev.1 은 B-7 **해법을 아예 명시하지 않았다**. 인용된 `cd "$REPO_ROOT" && git diff` 는 내 문서에 없는 문장이다. 그러나 "진단만 하고 처방을 안 썼다"는 것 자체가 결함이고, 제안된 `git add -N .`**동작한다**(실측) |
| C-2 | A-2 와 O-2/B-8 이 `reconcile.sh` 에서 충돌한다 | **채택 — 지목한 쌍은 존재하지 않음, 그러나 내 트랙 분해가 더 틀렸다** | `reconcile.sh``MAM_LOOP_MARKER` 참조 **0건**, `send_keys_safe` 참조 **0건** → O-2·B-8 은 `reconcile.sh` 를 건드리지 않는다. 반면 파일 단위 매트릭스를 만들어 보니 **내 §4.3 트랙 분해가 4곳에서 틀렸다** |
정정부터. Rev.1 §4.3 은 "트랙 C(정리)는 트랙 A/B 와 독립"이라고 썼다. **틀렸다.**
`B-6``run_loop.sh` 를 고치므로 B-7·O-2 와 같은 파일이고, `C-3a`·`C-4``lib.sh`
고치므로 B-8·A-4 와 같은 파일이다. agy 는 엉뚱한 쌍을 지목했지만 **"파일 단위 대조 없이
독립을 선언했다"는 지적의 실질은 옳고, 실제 피해는 그들이 본 곳보다 넓다.**
`git add -N .` 은 채택하되 **그대로는 쓰지 않는다.** 인덱스를 오염시켜
이후 `git commit -a`**작성자가 추가한 적 없는 파일을 조용히 커밋한다**(실측 §2.3).
Creator 에이전트가 같은 저장소에서 동시에 git 을 쓰는 구조라 이건 이론이 아니다.
**인덱스를 건드리지 않는 동등 대안**을 권한다(§2.4).
---
## 2. C-1 — B-7 해법: 채택, 기전 교체
### 2.1 전제 정정
챌린지는 "Plan §4 의 해법(`cd $REPO_ROOT && git diff`)"을 인용한다.
Rev.1 §4 표의 B-7 칸 전문은 다음과 같다:
> 리뷰어가 빈 diff 로 PASS. 신규 파일은 리뷰 대상 밖. **나머지 11건의 검증 근거를 훼손**(§3.2)
**근거만 있고 해법은 없다.** `cd "$REPO_ROOT"` 는 내가 쓴 적 없는 문장이다.
그러나 이건 방어가 아니라 자기 결함의 확인이다 — **P1-1 로 올려 놓고 처방을 안 썼다.**
구현자가 §3.2 의 두 원인 중 눈에 띄는 쪽(cwd)만 고치고 끝냈을 가능성이 크고,
챌린지는 정확히 그 시나리오를 예측했다. 처방을 명시하는 것으로 갚는다.
### 2.2 `git add -N .` 은 동작한다 (실측)
빈 저장소에 tracked 수정 1건 · untracked 신규 2건 · `.gitignore` 대상 1건을 심고 측정했다.
```
[before] git diff $BASE --stat
tracked.txt | 1 + ← 신규 파일 0건
[after] git add -N . ; git diff $BASE --stat
pkg/__init__.py | 1 + ← 잡힘
sub/newfile.py | 1 + ← 잡힘
tracked.txt | 1 +
ignored.log hunks: 0 ← .gitignore 존중됨
```
**제안의 두 가지 핵심 주장이 모두 참이다**: 미추적 신규 파일이 diff 에 포함되고,
`.gitignore` 는 그대로 존중된다.
### 2.3 그러나 인덱스가 오염된다 — 그리고 그게 커밋으로 샌다
`git add -N .` 직후 인덱스 상태:
```
A pkg/__init__.py
A sub/newfile.py
M tracked.txt
```
이 상태에서 Creator 가 `git commit -am "wip"` 을 실행하면:
```
$ git commit -qam "creator wip" ; git show --stat HEAD
pkg/__init__.py | 1 +
sub/newfile.py | 1 +
tracked.txt | 1 +
-> sub/newfile.py in that commit? brand new ← 내용까지 들어갔다
```
**작성자가 `git add` 한 적 없는 파일이 `-a` 한 번에 커밋된다.** 평소 `git commit -a`
미추적 파일을 건드리지 않으므로, 이건 **git 의 기본 안전 성질을 바꾸는 부작용**이다.
MAM 에서 이게 가설이 아닌 이유: `run_loop.sh` 는 Creator 에이전트가 **같은 저장소에서
동시에 작업하는 동안** 돌아간다. 루프가 인덱스를 바꾸는 시점과 Creator 가 git 을 쓰는
시점이 겹친다. 게다가 루프는 반복 실행되므로 오염이 매 사이클 재발한다.
`git reset` 으로 되돌리는 보정을 붙일 수도 있지만, (a) 비정상 종료 시 남고
(b) 되돌리는 순간과 Creator 의 git 호출이 또 경합한다. **부작용을 만들고 지우는 대신
애초에 만들지 않는 편이 낫다.**
### 2.4 권고: 인덱스를 건드리지 않는 동등 대안
```bash
CHANGES_DIFF=$(
cd "$REPO_ROOT" || exit 1
git diff "$BASE_COMMIT"
# 미추적 신규 파일: 인덱스를 바꾸지 않고 /dev/null 대비 diff 로 덧붙인다.
# --exclude-standard 가 .gitignore/.git/info/exclude 를 그대로 존중한다.
git ls-files -o --exclude-standard -z | while IFS= read -r -d '' f; do
git diff --no-index --binary /dev/null "$f" 2>/dev/null || true
done
)
```
같은 픽스처 실측:
```
diff --git a/tracked.txt b/tracked.txt ← 기존 파일 수정
diff --git a/sub/newfile.py b/sub/newfile.py ← 신규 파일
--- /dev/null
+++ b/sub/newfile.py
ignored.log present? 0 ← .gitignore 존중
[index] M tracked.txt / ?? sub/ ← 인덱스 무변경
```
동일한 결과를 내면서 인덱스를 건드리지 않는다.
> `git diff --no-index` 는 두 경로가 모두 저장소 밖일 때 rc=1 을 반환하지만,
> 여기서는 차이가 있을 때 rc=1 이 정상이므로 `|| true` 로 흡수한다.
> `-z` + `IFS= read -r -d ''` 는 공백·개행이 든 파일명을 위한 것이다.
### 2.5 함께 고쳐야 할 것 — `cd` 와 크기 상한
**(a) `cd "$REPO_ROOT"`** 는 여전히 필요하다. §3.2 의 두 원인 중 하나이고
서브셸 안에서 처리하면 호출자 cwd 를 오염시키지 않는다(위 코드에 반영).
**(b) 크기 상한이 없다.** 실측: `run_loop.sh:537,539` 에서 만든 `CHANGES_DIFF`
**아무 제한 없이** 547행의 리뷰 프롬프트 문자열에 그대로 보간되고,
그 프롬프트는 `send_keys_safe` 를 통해 TUI paste-buffer 로 주입된다.
미추적 파일을 포함시키면 diff 는 **커지기만 한다**. 누군가 큰 산출물을 ignore 하지 않은 채
남겨 두면 리뷰 주입이 통째로 실패하거나 잘린다.
**권고**: 상한(예: 200 KB / 4000 줄)을 두고 초과 시 `--stat` 요약 + 초과 사실 명시로 대체.
**잘렸다는 사실이 리뷰어에게 반드시 보여야 한다** — 조용히 잘리면 B-7 을
"빈 diff 로 PASS" 에서 "부분 diff 로 PASS" 로 바꾸는 것에 지나지 않는다.
---
## 3. C-2 — 트랙 경합: 지목한 쌍은 없고, 내 분해가 더 틀렸다
### 3.1 지목된 두 쌍은 성립하지 않는다
```
reconcile.sh 내 MAM_LOOP_MARKER / loop-guard-active 참조 → 0건
reconcile.sh 내 send_keys_safe / inject_instructions 참조 → 0건
```
- **O-2**: 마커는 `run_loop.sh:83-89` 에만 있다. `reconcile.sh`**자기 자신의 별도 락**
(`.mam/monitor.lock`, `fcntl.flock`, 86-95행)을 이미 갖고 있다 — 다른 프로세스를 위한
다른 뮤텍스다. O-2 의 처방은 `run_loop.sh` 안에서 끝난다.
- **B-8**: `send_keys_safe``lib.sh` 함수이고 `reconcile.sh` 는 이를 호출하지 않는다.
따라서 "A-2 ⟂ O-2/B-8 이 `reconcile.sh` 에서 충돌"은 **실재하지 않는다.**
### 3.2 그러나 Rev.1 §4.3 은 실제로 틀렸다
챌린지가 제기한 방법론적 문제 — **파일 단위 대조 없이 독립을 선언했다** — 는 옳다.
각 항목의 처방이 건드리는 파일을 근거에서 도출해 매트릭스를 만들었다.
| 파일 | 건드리는 항목 |
|---|---|
| `run_loop.sh` | **B-6, B-7, O-2** |
| `lib.sh` | **A-4, B-8, B-10, C-3a, C-4** |
| `reconcile.sh` | **A-2, A-4, B-10** |
| `mqtt_common.py` | **A-2, B-9** |
| `stop_session.sh` | **B-10, C-6** |
| `registry.py` | **A-2, C-4** |
| `create_session.sh` | **A-4, C-4** |
Rev.1 §4.3 의 오류 4건:
1. **`B-6` 을 트랙 C(독립)에 뒀다.** `run_loop.sh` 이므로 B-7·O-2 와 같은 파일이다.
2. **`C-3a`·`C-4` 를 트랙 C(독립)에 뒀다.** `lib.sh` 이므로 B-8·A-4 와 같은 파일이다.
3. **`B-9` 를 P5 독립으로 뒀다.** `mqtt_common.py` 이므로 A-2 와 같은 파일이다.
4. **`C-6` 을 독립으로 뒀다.** `stop_session.sh` 이므로 B-10 과 같은 파일이다.
agy 가 지목한 A-2↔O-2 는 없지만 **A-2↔A-4, A-2↔B-10, A-2↔B-9, A-2↔C-4** 는 있다.
`lib.sh` 는 5개 항목이 몰리는 최대 경합 지점이다.
### 3.3 결론: "트랙"이 아니라 "파일 소유권"으로 직렬화한다
트랙 개념 자체가 잘못된 추상화였다. 병렬 단위를 **주제**가 아니라 **파일**로 잡는다.
| 파일 소유 슬롯 | 순서 | 동시 실행 가능 |
|---|---|---|
| **`run_loop.sh`** | B-7 → O-2 → B-6 | 다른 슬롯과 병렬 |
| **`lib.sh`** | C-3a+C-4 → B-8 → (A-4 M0~) | 다른 슬롯과 병렬 |
| **MQTT 계열**(`mqtt_common.py`·`registry.py`·`publish_event.py`·`job_subscriber.py`·`reconcile.sh`) | A-2 → B-9 | 다른 슬롯과 병렬 |
| **`stop_session.sh`** | C-6 → (B-10) | 다른 슬롯과 병렬 |
- 한 슬롯 안은 **직렬**, 슬롯 간은 **병렬**. 슬롯을 넘는 항목(**A-4**, **B-10**)은
**단독 실행**한다 — A-4 는 `lib.sh`+`reconcile.sh`+`create_session.sh`,
B-10 은 `lib.sh`+`reconcile.sh`+`stop_session.sh` 이므로 어떤 슬롯 조합과도 겹친다.
- `reconcile.sh` 를 MQTT 슬롯에 넣은 이유: A-2 가 그 파일에서 가장 큰 변경을 하고,
나머지 두 소비자(A-4·B-10)는 어차피 단독 실행이다.
**우선순위 표(Rev.1 §4)의 순위 자체는 바뀌지 않는다.** 바뀌는 것은 병렬화 방식뿐이다.
---
## 4. 변경 요약 (Rev.1 대비)
| ID | 대상 | 내용 |
|---|---|---|
| R-1 | B-7 처방 (신규) | `cd "$REPO_ROOT"` + `git ls-files -o --exclude-standard` 기반 미추적 파일 덧붙이기. **`git add -N` 은 채택하지 않음**(인덱스 오염, §2.3) |
| R-2 | B-7 처방 (신규) | `CHANGES_DIFF` 크기 상한 + **잘림 사실 명시** |
| R-3 | §4.3 교체 | "트랙" → **파일 소유권 슬롯**. Rev.1 의 독립 선언 4건 정정 |
| R-4 | A-4 · B-10 | 슬롯 경계를 넘으므로 **단독 실행** 명시 |
우선순위(P0-1 ~ P5, 종결 권고 B-5)와 §3 실측 결과는 **전부 그대로 유효**하다.
---
## 5. 검증
전부 임시 저장소(`scratchpad/b7`, `b7b`)에서 실측했다. 프로덕션 저장소의 인덱스는
**건드리지 않았다** — 인덱스 오염이 바로 이 논점이므로 실 저장소에서 재현하는 것은 부적절하다.
| 검증 | 결과 |
|---|---|
| `git add -N .` 이 미추적 파일을 diff 에 포함시키는가 | ✅ 포함 (2/2 신규 파일) |
| `.gitignore` 존중 | ✅ `ignored.log` 0 hunks |
| 인덱스 잔존 여부 | ❌ `A pkg/__init__.py`, `A sub/newfile.py` 잔존 |
| 잔존 상태에서 `git commit -a` | ❌ **추가한 적 없는 파일이 내용째 커밋됨** |
| 대안(`ls-files -o` + `--no-index`) 포함 여부 | ✅ 포함 |
| 대안의 `.gitignore` 존중 | ✅ 0 hunks |
| 대안의 인덱스 영향 | ✅ 무변경 (`?? sub/` 유지) |
| `reconcile.sh` 의 O-2 심볼 참조 | 0건 → C-2 전제 반증 |
| `reconcile.sh` 의 B-8 심볼 참조 | 0건 → C-2 전제 반증 |
| `CHANGES_DIFF` 크기 상한 | 없음 (537·539 → 547 무제한 보간) |
---
## 6. 남는 불확실성
Rev.1 §6 의 4건(A-2 노출도 · O-2 경합 창 · B-9 호출자 전수 · A-4 상한)은 그대로 유효하다. 추가분:
**6.5 크기 상한값은 근거 없이 제시했다.** §2.5 의 "200 KB / 4000 줄"은 관례적 수치이지
측정값이 아니다. `send_keys_safe` 의 paste-buffer 가 실제로 어느 크기에서 실패하는지는
측정하지 않았다 — 실 세션에 대용량 주입을 시도하는 실험이라 Planner 범위에서 부적절하다.
**B-7 구현자가 샌드박스 세션에서 상한을 측정해 확정할 것.**
**6.6 파일 매트릭스는 처방 기준의 추정이다.** 각 항목이 실제로 어느 파일을 건드릴지는
구현 단계에서 늘어날 수 있다(특히 테스트 파일). 슬롯 배치는 구현 착수 시 재확인해야 한다.
**6.7 `git ls-files -o` 는 서브모듈·심링크를 이 저장소에서 검증하지 않았다.**
MAM 저장소에는 서브모듈이 없어 실측 대상이 아니었다. 다른 워크스페이스에 배포될 때를
고려하면 구현자가 한 번 확인하는 편이 좋다.
---
## 7. 결론
두 챌린지 모두 **전제는 틀렸고 결론은 맞다.**
C-1 이 인용한 `cd $REPO_ROOT && git diff` 는 내 문서에 없다 — 나는 B-7 의 처방을
**아예 쓰지 않았다**. 그게 더 나쁘다. 제안된 `git add -N .` 은 실제로 동작하지만
인덱스를 오염시켜 `git commit -a` 가 추가한 적 없는 파일을 커밋하게 만든다.
동등하면서 부작용 없는 형태로 교체해 채택한다.
C-2 가 지목한 A-2↔O-2/B-8 충돌은 `reconcile.sh` 참조 0건으로 **존재하지 않는다**.
그러나 파일 매트릭스를 만들어 보니 **내 트랙 분해가 4곳에서 틀렸고**,
`lib.sh` 에는 5개 항목이 몰려 있었다. 트랙이라는 추상화를 버리고 파일 소유권 슬롯으로 바꾼다.
우선순위 순서 자체는 Rev.1 그대로다. 바뀐 것은 **B-7 의 처방**과 **병렬화 방식** 두 가지다.
**[AGREEMENT: REACHED]**