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

210 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔍 교차 코드 리뷰 — 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_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` — 범위 밖 변경이고 조건이 **항상 거짓**
```diff
- 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` 디버그 출력 제거 — 범위 밖이지만 개선
```diff
- 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]