210 lines
13 KiB
Markdown
210 lines
13 KiB
Markdown
# 🔍 교차 코드 리뷰 — 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]
|