Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-5b570f5a.md
T

13 KiB
Raw Blame History

🔍 교차 코드 리뷰 — Job 5b570f5a

  • 역할: Reviewer
  • 대상: --herdr-workspace 도입 및 레거시 폴백 체인 분리 (계획 5801cbe2 Rev.2 구현분) — 워킹 트리 14파일 (+531 / 47)
  • 기준 커밋: 320f036 / 미추적 파일 0건

1. 결론

계획 Rev.2 의 S1~S10 이 전부 사양대로 구현됐고, D2 게이트와 D5 호출자 집합까지 정확히 지켜졌습니다. 뮤테이션 12종 전부 지정 테스트를 FAIL 시키며, 계획이 열어 뒀던 두 개의 미확인 항목(정적 가드의 실효성, M3b/M3c 판별력)이 모두 실증됐습니다.

차단 사유 없음. 다만 브리프·계획 어디에도 없는 변경 1건reconcile.sh 입양 가드에 들어갔고 그 조건이 항상 거짓입니다(§5 F-1). 라이브 회귀는 아니지만 커밋 전에 정리할 것을 권합니다.

따라서 통과 처리합니다(최종 태그는 보고서 마지막 줄).


2. 검증 결과

검증 결과
전체 스위트 362 passed / 1 failed / 484.04s — 실패 1건은 §3 참조
수집 수 346 → 363 (신설 17개 노드)
신설 17건 대조군 17 passed
bash -n 8개 변경 스크립트 8/8 OK
D2 게이트 생산 코드의 resolve_herdr_workspace 호출자 = update_yaml_resumed.sh:57 단 1곳 — D5 가 지정한 그대로
D5 준수 create_session.sh 는 함수를 쓰지 않고 ${ws_slug#mam-} 로 직접 계산 (주석으로 이유 명기)
뮤테이션 12/12 검출

계획은 359 를 예상했는데 실제는 363 입니다. 차이 4는 test_slug_parity_between_bash_and_python@parametrize 4개로 4개 노드가 되기 때문입니다 — 제 계획의 산수 오류이지 구현 문제가 아닙니다.


3. 스위트 실패 1건 — 이번 변경분과 무관

FAILED tests/test_deploy_freshness.py::test_d23_compose_image_matches_doc_and_is_alpine
E   AssertionError: Compose image tag '2.14-alpine' not found in PRIVATE_SERVER.md
E   assert '2.14-alpine' in ['2.12-alpine', '2.12-alpine', '2.12-alpine']

nats-docker 서브모듈 내부의 드리프트입니다.

nats-docker/docker/docker-compose.yaml:9   image: nats:2.14-alpine
nats-docker/PRIVATE_SERVER.md:106,116,439  nats:2.12-alpine   (3곳)

이번 변경분과 무관함을 구조적으로 확정할 수 있습니다.

$ git diff --stat HEAD -- tests/test_deploy_freshness.py nats-docker deploy/
(출력 없음)

이 테스트와 그 입력 파일이 전부 HEAD 와 동일하므로 결과도 HEAD 와 동일합니다. 즉 선재 실패입니다.

브리프 목표 ③은 "Ensure full pytest suite passes" 라고 적혀 있고 스위트는 100% 통과하지 않습니다. 그 사실은 그대로 기록하되, 원인이 이 변경분 밖에 있으므로 차단 사유로 삼지 않습니다. 서브모듈 태그 동기화는 별건입니다(§6 N-1).


4. 뮤테이션 매트릭스 — 12/12 검출

격리 사본(.git + nats-docker 포함 — 계획 §8 측정 주의 반영). 대조군 17/17 통과.

# 뮤테이션 결과
M1 lib.sh 소켓 lookup 에 herdr_workspace 재도입 ..._never_resolves_as_socket + 정적 가드 2건 FAILED
M2 resolve_herdr_workspace 를 다시 별칭으로 ..._are_decoupled + ..._prefers_the_row... 2건 FAILED
M3b ②③ 순서를 Rev.1 로 되돌림 ..._prefers_the_row... FAILED / ..._uses_the_argument... PASSED
M3c ③ 분기 삭제 (과잉 교정) ..._prefers_the_row... PASSED / ..._uses_the_argument... FAILED
M4 reconcile.sh drift A 에 폴백 재도입 정적 가드 FAILED (..._never_resolves_as_socket 은 정상적으로 PASSED — lib.sh 는 안 건드렸으므로)
M5 create 파서가 값 폐기 2건 FAILED
M6 기본값을 ${ws_slug} (접두사 유지) FAILED
M6b env 폴백 제거 FAILED
M7 MAM_WS_LABELSTART_CMD 에 주입 FAILED
M8 resume 주 경로(:141-142)에서 --herdr-workspace 미전달 FAILED
M9 신규 행 dict 에서 herdr_workspace 제거 FAILED
M10 create 가 resolve_herdr_workspace 를 쓰도록 (D5 위반) FAILED
M11 입양 dict 에서 herdr_workspace 제거 FAILED
M12 status.sh 가 두 컬럼에 같은 값 출력 FAILED

계획이 열어 뒀던 두 항목이 닫혔습니다

① M3b 와 M3c 가 서로 다른 단언을 깹니다. 계획이 수용 조건으로 못박은 성질입니다 — 순서 역전(M3b)과 과잉 교정(M3c)이 각각 다른 단언에 걸립니다. T3b 가 한쪽만 보는 테스트가 아니라는 뜻이고, J-2 에서 n=3 을 골라 M6 을 판별하지 못했던 실수가 반복되지 않았습니다.

② 정적 가드가 M4 를 실제로 검출합니다. 계획 §6 은 "M4 를 실제로 검출하는지 뮤테이션으로 확인하는 것을 수용 조건에 넣습니다" 라고 적었습니다. 인라인 Python 4개 지점은 lib.sh 해석기를 거치지 않아 단위 테스트로는 안 잡히는데, 소스 수준 가드가 정확히 그 자리를 덮습니다. 문자열 가드로서는 드물게 감도가 실증된 경우입니다.

부수 확인 — 조건부 플래그 전달의 단어 분할

resume_session.sh 가 쓰는 ${HERDR_WORKSPACE_OPT:+--herdr-workspace "$HERDR_WORKSPACE_OPT"} 는 통상 공백 포함 값에서 깨지기 쉬운 형태라 별도 확인했습니다.

VAR=[has space]  ->  arg3=[--herdr-workspace]  arg4=[has space]     (배열 형태와 동일)
VAR=[]           ->  플래그 자체가 사라짐

bash 가 :+ 워드 안에서 따옴표 제거를 수행하므로 공백이 보존됩니다. 안전합니다.


5. 발견 사항

🟠 F-1 (P2): reconcile.sh:511 — 범위 밖 변경이고 조건이 항상 거짓

-        if name in yaml_session_names or any(_sanitize(y) == name for y in yaml_session_names):
+        srv = t.get('server', 'default')
+        if (name, srv) in yaml_session_names or any(_sanitize(y) == name for y in yaml_session_names):

yaml_session_names문자열 집합입니다(:480 {s['name'] for s in ...}). 튜플은 이 집합에 절대 들어 있을 수 없습니다.

(name, srv) in {문자열들}  ->  False
name in {문자열들}         ->  True

바로 위 :482alive_set 이 실제로 튜플 집합이라({(t['name'], t.get('server','default')) ...}) 그 패턴을 옮겨 온 것으로 보입니다. 의도는 소켓별 중복 판정인데 구현이 무동작입니다.

라이브 회귀는 아닙니다. 남은 _sanitize 분리항이 옛 exact match 를 흡수하기 때문입니다 — _sanitize 가 멱등임을 실측했고(3/3), MAM 이 만든 세션은 시프트가 생성 시 sanitize 하므로 herdr ls 가 돌려주는 이름과 _sanitize(YAML 이름) 이 일치합니다.

라이브 세션명 len=46: canary-projects-multi-agent-mux-creator-claude
_sanitize      len=32: canary-projects-multi-a-039bb460      → herdr 쪽 이름과 일치

남는 틈은 MAM 밖에서 만들어진 32자 초과 이름의 세션이 그 긴 이름 그대로 YAML 에 수기 등록된 경우뿐입니다. 이때 _sanitize(y) != name 이라 가드가 뚫려 이미 등록된 세션을 중복 입양합니다. 좁지만 도달 가능합니다.

그리고 이 가드에는 테스트가 0건입니다. 분리항까지 제거해 가드를 완전히 죽인 사본으로 측정:

tier2 + tier3 with the adoption guard fully dead  ->  45 passed

즉 어느 쪽으로 바꿔도 스위트는 초록입니다. 검증이 불가능한 상태에서 범위 밖 변경이 들어간 셈입니다.

권고: 이번 커밋에서는 원래 형태로 되돌리십시오 — if name in yaml_session_names or any(...). 나머지 리팩터(srv 호이스팅, :531 에서의 재사용)는 순수 정리이므로 유지해도 좋습니다. 소켓별 중복 판정이 실제로 필요하면 yaml_session_names 를 튜플 집합으로 바꾸는 별도 변경으로 다루고(:480·:605 동시 수정 + 전용 테스트), 그 자체가 행동 변경이므로 근거를 따로 세워야 합니다(§6 N-2).

🟡 F-2 (P3): stop_session.sh usage 가 "recorded" 라고 하지만 아무것도 기록하지 않는다

  --herdr-workspace <name> — recorded label only; never selects a socket

HERDR_WORKSPACE_OPT 는 선언(:70)과 파싱(:83) 두 곳에만 등장하고 이후 어디에도 쓰이지 않습니다. stop 은 YAML 을 쓰므로 "기록"이 가능한데도 하지 않습니다.

같은 저장소의 multi-agent-mux-stop/SKILL.md 는 정확하게 적혀 있습니다 — "CLI 대칭성을 위해 파서에서 허용되지만 소켓 라우팅에는 영향을 주지 않습니다." 즉 두 문서가 서로 다른 말을 합니다.

이 문구는 제 계획(§4.6)에서 나온 것이므로 계획의 표현 결함입니다. 구현은 계획 본문의 의도("인자 호환성 확보가 목적")를 정확히 따랐습니다. 처방은 둘 중 하나입니다 — usage 를 SKILL.md 와 같은 표현("accepted for symmetry; not recorded")으로 고치거나, stop 의 YAML 쓰기에 실제로 기록하거나. 전자를 권합니다(stop 이 라벨을 재정의하는 것은 D6 취지에 어긋납니다).

🟡 F-3 (P3): reconcile.sh 디버그 출력 제거 — 범위 밖이지만 개선

-        import sys
-        sys.stderr.write(f"LS CMD: {cmd} | RC: {r.returncode} | ...")
-except Exception as ex:
-    import sys
-    sys.stderr.write(f"EX IN RECONCILE LS: {ex}\n")
+except Exception:

매 사이클마다 stderr 로 나가던 개발 잔재입니다. 제거가 옳지만 브리프·계획 어디에도 없습니다. except Exception as exexcept Exception 은 동작 보존입니다. F-1 과 함께 "이 커밋이 범위 밖 정리를 몇 건 포함한다"는 사실만 기록합니다.


6. 계획 대비 이행 점검

항목 이행
S1 폴백 항 제거 6곳 각 지점에 계획이 지정한 근거 주석 포함
S2 호출자 이관 + 기존 테스트 2건 정정 함수명과 호출 대상이 처음으로 일치
S3 resolve_herdr_workspace 재정의 C-1 순서(라벨 → pane.cwdws) 그대로, 주의 1·2 주석 포함
S4 create (--herdr-workspace + C-3 env + D5) MAM_WS_LABEL 로 내부 변수명 분리까지 반영
S5 resume 계열 (양쪽 호출 지점) :73-76, :139-142 둘 다 전달
S6 stop 파서·usage (F-2 문구 제외)
S7 status 컬럼 분리 SOCKET / WORKSPACE 분리, JSON 에 herdr_workspace 추가
S8 문서 3종 + resume/SKILL.md:76
S9 테스트 17개 노드
S10 입양 행 (C-2 + K-2) herdr_server + herdr_workspace 동시 추가
D1 순서 — 커밋 미분할 상태로 리뷰. 계획의 7분할은 커밋 시 적용 필요

tests/conftest.pystate["calls"].appendstate.setdefault("calls", []).append 은 모의 herdr 의 방어적 수정으로, 생산 결함을 가릴 수 없는 형태입니다.


7. 후속 (범위 밖)

ID 내용
N-1 nats-docker 서브모듈의 docker-compose.yaml(2.14-alpine) ↔ PRIVATE_SERVER.md(2.12-alpine) 태그 드리프트 — test_d23 실패 원인, 선재
N-2 drift-B 입양 가드의 소켓별 중복 판정 — F-1 의 "완성된 형태". yaml_session_names 를 튜플 집합으로 전환 + 전용 테스트
N-3 drift-B 입양 가드에 테스트 0건 (§5 F-1 실측: 가드를 완전히 죽여도 45 passed)
K-1 test_o2_18_orphan_steal_lock_recovered 부하 민감 플레이크 (이번 실행에서는 미발생)
K-3 reconcile.shherdr -L <srv> 직접 호출이 심의 --session 경로를 우회
K-4 README.md / README.ko.md 의 구 herdr -L <server> 서술
K-5 create_session.sh:216HERDR_SERVER_OPT 가드 무동작
K-6 stop_session.sh--workspace 파서 부재

8. 총평

계획 Rev.2 가 챌린지 라운드를 거치며 벼려진 세 지점 — C-1 순서, D5 호출자 집합, 정적 가드 — 이 전부 구현에서 값어치를 증명했습니다. 특히 M3b/M3c 가 서로 다른 단언을 깨는 것과 정적 가드가 M4 를 잡는 것은 계획이 수용 조건으로 미리 못박아 둔 성질이었고, 둘 다 실측으로 충족됐습니다.

남은 것은 범위 밖 변경 3건입니다. F-3 은 순수 개선이고, F-2 는 제 계획의 표현 결함이며, F-1 만 실제로 손볼 값어치가 있습니다 — 무동작 조건이 파괴적 경로(입양·terminated 판정)에 들어가 있고 테스트가 0건이라, 지금은 무해해도 다음 사람이 _sanitize 분리항을 건드리는 순간 살아나는 종류입니다. 한 줄 되돌림이면 됩니다.

[VERDICT: PASS]