13 KiB
🔍 교차 코드 리뷰 — Job 5b570f5a
- 역할: Reviewer
- 대상:
--herdr-workspace도입 및 레거시 폴백 체인 분리 (계획5801cbe2Rev.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이@parametrize4개로 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_LABEL 을 START_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
바로 위 :482 의 alive_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 ex → except Exception 은 동작 보존입니다. F-1 과 함께 "이 커밋이 범위 밖 정리를 몇 건 포함한다"는 사실만 기록합니다.
6. 계획 대비 이행 점검
| 항목 | 이행 |
|---|---|
| S1 폴백 항 제거 6곳 | ✅ 각 지점에 계획이 지정한 근거 주석 포함 |
| S2 호출자 이관 + 기존 테스트 2건 정정 | ✅ 함수명과 호출 대상이 처음으로 일치 |
S3 resolve_herdr_workspace 재정의 |
✅ C-1 순서(라벨 → pane.cwd → ws) 그대로, 주의 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.py 의 state["calls"].append → state.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.sh 의 herdr -L <srv> 직접 호출이 심의 --session 경로를 우회 |
| K-4 | README.md / README.ko.md 의 구 herdr -L <server> 서술 |
| K-5 | create_session.sh:216 의 HERDR_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]