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

14 KiB

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.shMAM_LOOP_MARKER 참조 0건, send_keys_safe 참조 0건 → O-2·B-8 은 reconcile.sh 를 건드리지 않는다. 반면 파일 단위 매트릭스를 만들어 보니 내 §4.3 트랙 분해가 4곳에서 틀렸다

정정부터. Rev.1 §4.3 은 "트랙 C(정리)는 트랙 A/B 와 독립"이라고 썼다. 틀렸다. B-6run_loop.sh 를 고치므로 B-7·O-2 와 같은 파일이고, C-3a·C-4lib.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 권고: 인덱스를 건드리지 않는 동등 대안

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_safelib.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]