# 🔍 교차 코드 리뷰 리포트 — Job `e8cee19e`

- **Job ID**: `e8cee19e`
- **리뷰어**: `planner-reviewer-claude-01` (Reviewer)
- **작성일**: 2026-08-27
- **대상 브랜치/기준 커밋**: `main` @ `4bbd03b`
- **리뷰 범위**: 워킹트리 누적 변경분 (`git diff`) — 4 files, +808 / −92
  - `.agents/skills/lib.sh` (+289/−…)
  - `tests/conftest.py`
  - `tests/test_herdr_shim_contract.py`
  - `tests/test_b19_headless_reconcile_fixes.py`
- **근거 문서**: `bug_report.md` (v1.0), `.agents/reports/planner-reviewer-claude-01/plan-fae58b93.md`

---

## 0. 요약 (TL;DR)

계획서(`plan-fae58b93.md`)에 명시된 **F-1 ~ F-4 및 워크스페이스 세션 격리(ISSUE-1/2/3/5)** 4개 항목이 모두 코드에 반영되어 있으며, 전체 회귀 테스트가 **412 passed (실측 699.09s, exit 0)** 로 통과함을 리뷰어 환경에서 **재실행하여 직접 확인**했다.

린트(구문), 동작성, 유실(회귀) 세 관점 모두에서 **머지를 막을 결함(blocker)은 발견되지 않았다.** 다만 설계상 의도적으로 남겨진 잔여 스코프 갭 3건을 **비차단 후속 과제(Non-blocking)** 로 기록한다.

---

## 1. 검증 방법

| # | 검증 항목 | 방법 | 결과 |
|---|---|---|---|
| V-1 | 전체 회귀 테스트 | `.venv/bin/python -m pytest tests/ -q` (리뷰어가 직접 재실행) | ✅ **412 passed in 699.09s**, exit 0 |
| V-2 | 셸 구문 린트 | `bash -n .agents/skills/lib.sh` | ✅ 통과 (오류 없음) |
| V-3 | 생성 shim 구문 린트 | `bash -n $WORKSPACE_ROOT/.mam/shim/herdr` (H-19 테스트 내장) | ✅ 통과 |
| V-4 | Bash 3.2 호환성 | 로컬 `GNU bash 3.2.57 (arm64-apple-darwin25)` 에서 `local arr=()` + `"${a[@]+"${a[@]}"}"` 패턴 실측 | ✅ `set -euo pipefail` 하에서 정상 동작 |
| V-5 | 정적 교차 검토 | diff 전량 + 인접 컨텍스트(`lib.sh` 380~440, 609~730, 1926~2070행) 직접 판독 | ✅ 계획서와 구현 일치 |
| V-6 | 호출자 영향 분석 | `send_keys_safe` 호출부 grep (`stop_session.sh:204`, `delegate-job:527`) | ✅ 신규 rc=3 를 모두 비정상으로 처리 |

> **참고**: shellcheck 는 본 환경에 미설치되어 실행하지 못했다. 대신 `bash -n` 2종(원본 + 생성 shim) + Bash 3.2 실측으로 대체했다. 이는 CI 게이트가 아니므로 차단 사유가 아니다.

---

## 2. 작업 목표별 반영 확인

### ✅ F-1 — `Yes, try it` 토큰 정리 및 회귀 테스트

- `_MAM_DIALOG_TOKENS` 에서 `|Yes, try it` 제거 확인 (`lib.sh:62`).
- `handle_startup_dialogs` 의 Escape 거부 분기(`lib.sh:2048`)는 **그대로 보존** — 실제 fullscreen 업셀 모달은 여전히 Escape 로 거부된다. 즉 "탐지 완화"가 아니라 **책임 분리**(대화형 산문 오탐 제거 / 실제 모달 처리 유지)로 올바르게 구현되었다.
- 회귀 고정: `test_prose_yes_try_it_is_not_a_dialog`(산문 → DIALOG_CLOSED), `test_mam_dialog_tokens_exclude_yes_try_it`(토큰 라인 + Escape 분기 동시 검증), `test_fullscreen_modal_is_rejected_not_accepted`(Escape 전송·Enter 미전송).
- **판정**: 반영 완료. 오탐(산문으로 인한 `send_keys_safe` rc=2 데드락)과 정탐(모달 거부)이 양방향으로 고정되었다.

### ✅ F-2b — `_herdr_agent_get_scoped` 스코핑 단축경로 보완

- 기존의 스코프 없는 존재 확인(`_real_herdr agent get "$x" >/dev/null 2>&1`)이 `has-session`, `kill-session`, `capture-pane`, `send-keys`, `paste-buffer`, `agent` 6개 분기에서 전부 제거되고 `_herdr_agent_get_scoped` / `_resolve_herdr_pane_id` 로 대체됨.
- `_resolve_herdr_target` 에 **명시적 env 미스 시 raw 폴스루 차단**(`HERDR_WORKSPACE_ID` 설정 시 `return 1`)이 추가됨 — 이것이 F-2b 의 핵심이며, `agent prompt` 가 타 워크스페이스 동명 에이전트로 프롬프트를 배달하는 경로를 실제로 닫는다.
- 호출부 3곳(`prompt`/`get`/`read`)이 `$(... || true)` + 빈 문자열 검사 + `exit 1` 로 **실패를 삼키지 않고 상위 전달**한다. `set -e` 하에서 명령 치환 실패로 셸이 죽지 않도록 `|| true` 가 일관되게 붙어 있다 — 계약(주석에 명시)과 구현이 일치.
- 회귀 고정: `test_h21_has_session_agent_get_is_workspace_scoped`(w2 → rc=1, w1 → rc=0, unset → 전역 유지), `test_h22_agent_prompt_does_not_cross_workspace`(out-of-scope 시 `agent prompt` 호출 자체가 0건임을 mock call log 로 검증).
- **판정**: 반영 완료. 특히 H-22 가 "에러만 났는지"가 아니라 **부작용(RPC 호출)이 발생하지 않았음**을 검증하는 점이 좋다.

### ✅ F-3 — H-19 테스트 정밀화

- `test_h19_single_resolver_helper_used_by_all_branches` 가 단순 문자열 카운트에서 **case arm 단위 파싱(`_case_arm`)** 으로 정밀화됨.
- 검증 강도: (a) 헬퍼 정의 1회 유일성, (b) 5개 분기 전부 헬퍼 호출, (c) 각 분기에 **스코프 없는 `agent get … >/dev/null` 잔존 금지** 정규식, (d) `agent` arm 의 `$sat`/`$raw` 단축경로 제거, (e) 헬퍼 블록 밖 `_herdr_agent_get_scoped()` 재정의 0건, (f) `bash -n` 2종.
- `list-panes` arm 을 `elsewhere` 에서 제외한 처리도 타당하다(해당 arm 은 정상적으로 pane list 를 직접 다룬다).
- **판정**: 반영 완료. "헬퍼는 만들었지만 옛 경로가 살아있다"는 회귀를 구조적으로 차단한다.

### ✅ F-4 — capture-pane 폴백 복원

- `capture-pane` arm 이 `pane read <pane_id>` → `agent read <sat>` → `agent read <sess>` 3단 폴백으로 복원됨. pane_id 해석 실패 시에도 기존 agent-level 경로가 살아있어 **유실 없음**.
- 동일 패턴이 `send-keys`(pane 실패 시 agent 이름 폴백), `kill-session`(pane close + kill-session 2단)에도 유지된다.
- **판정**: 반영 완료. F-2b 의 엄격화로 인한 기능 유실 위험이 이 폴백으로 상쇄된다.

### ✅ ISSUE-1 — 이중 제출(double-submit) 차단

- `paste-buffer` 가 존재하지 않는 `agent send` 대신 **`pane send-text` 삽입 전용**으로 교체되었고, Enter/C-m 을 일절 보내지 않는다. 제출 책임은 `send_keys_safe` 가 단독 소유.
- pane 미해석 / send-text 실패 시 `|| true` 로 삼키지 않고 `exit 1` → `send_keys_safe` 가 `rc=3` 반환. **빈 프롬프트 제출** 시나리오가 닫혔다.
- 고속 경로(`agent prompt`, 원자적 텍스트+제출)와 폴백 경로(send-text → C-m)가 상호 배타적이므로 제출은 정확히 1회.
- 회귀 고정: `test_h15_paste_buffer_inserts_without_enter`(send-text 정확히 1건, 제출 계열 호출 0건), `test_h16_send_keys_safe_submits_exactly_once`(Enter/C-m 정확히 1건), `test_d5 / test_send_keys_safe_returns_3_when_paste_buffer_fails`(rc=3 + 버퍼 정리 수행 + C-m 미전송), `test_paste_buffer_branch_never_submits`(소스 레벨 토큰 금지).
- **판정**: 반영 완료. 특히 **실패 시에도 `delete-buffer` 를 먼저 수행한 뒤 rc 를 반환**하는 순서가 정확하다(버퍼 누수 없음).

### ✅ ISSUE-2 — substring 오라우팅 제거

- `_resolve_herdr_target` 의 `(not name and agent and agent in tn)`, `has-session` 의 `(not an and a.get("agent") and a.get("agent") in tn)` 두 substring 분기가 모두 제거되고 **exact match only** 로 대체.
- `_resolve_herdr_pane_id` 의 pane list 매칭도 `label` → `name` → `agent` 순 **정확 일치**(원본/sanitized 양쪽 후보)만 수행.
- 회귀 고정: `test_h17_no_substring_cross_pane_routing`(`reviewer-creator-grok-01` 이 `agent: "grok"` 패인에 매칭되지 않음 + 동종 에이전트 2개 중 정확한 패인으로만 send-keys), `test_no_substring_matching_remains_in_lib_sh`(소스 레벨 `\bin tn\b` 잔존 0건).
- **판정**: 반영 완료.

### ✅ ISSUE-3 — 워크스페이스 세션 격리

- 3단 스코프 소스: `HERDR_WORKSPACE_ID` (env, 하드) → `$WORKSPACE_ROOT/.mam/herdr_workspace_id` (persist) → 없으면 기존 서버 전역 조회.
- **cwd 추론을 하지 않는다**는 결정이 주석에 명시되어 있고 구현도 일치 — 정당한 교차 워크스페이스 조회를 조용히 막지 않는다.
- `pane split` 에 `--env HERDR_WORKSPACE_ID="$existing_ws"` 주입, `workspace create` 경로에서는 응답에서 `workspace_id` 를 파싱해 `export` + 파일 영속화.
- `pane list --workspace` 서버측 필터 + **파이썬 클라이언트측 재필터**(구버전 herdr 가 `--workspace` 를 무시할 경우 대비) 이중화 — 방어적으로 잘 설계됨.
- 회귀 고정: `test_h18_workspace_scoped_pane_resolution`(w1/w2 동명 라벨 분리 라우팅, unset 시 전역 유지), `test_h23_persisted_workspace_id_scopes_without_env`(env 없이 파일만으로 스코핑 + new-session 이 파일을 실제로 기록).
- **판정**: 반영 완료. 잔여 갭은 §4 참조.

### ✅ ISSUE-5 — 파서 중복 제거

- `agent get → pane_id` 를 파싱하던 인라인 python heredoc 3벌(`kill-session`, `send-keys`, 구 `capture-pane`)이 전부 제거되고 `_resolve_herdr_pane_id` 단일 진입점으로 수렴.
- pane_id 정규식이 계획서 §1.2 의 지적대로 `^w[A-Za-z0-9]+:p[A-Za-z0-9]+$` 로 채택됨 — 버그 리포트의 `^w[0-9]+:p[0-9]+$` 를 그대로 썼다면 `w1E:p1` 형태의 **실제 pane_id 를 전량 거부**했을 것이다. 이 수정은 정확하며, `test_h20_pane_id_regex_accepts_alphanumeric_workspace` 로 accept/reject 5케이스가 고정되어 있다.
- **판정**: 반영 완료. 리뷰 과정에서 가장 위험했던 함정을 계획 단계에서 잡아낸 점이 확인된다.

---

## 3. 테스트 하네스(`conftest.py`) 변경 검토

mock herdr 변경이 "테스트를 통과시키기 위한 눈속임"이 아니라 **실제 herdr CLI 계약에 더 가깝게 교정**하는 방향인지를 중점 확인했다.

| 변경 | 평가 |
|---|---|
| `agent send` 서브커맨드 **제거** + 미지원 서브커맨드 `exit 1` | ✅ **정확한 교정**. 실제 `herdr agent --help` 에 `send` 가 없다. 기존 mock 이 존재하지 않는 명령을 성공시켜 ISSUE-1 을 은폐하고 있었다. |
| `agent prompt` 가 미매칭 시 `exit 1` (기존: 항상 ok) | ✅ 정확한 교정. 무조건 성공하던 mock 이 F-2b 검증을 불가능하게 만들고 있었다. |
| `pane send-text` / `pane read` / `pane rename` 추가 | ✅ 신규 코드 경로가 실제로 사용하는 명령이며, agents/panes 양쪽 저장소를 모두 조회하는 구현이 shim 의 폴백 구조와 대칭이다. |
| `pane list` 를 agents ∪ panes **병합 + pane_id 중복 제거** 로 변경 | ✅ 필요한 수정. 기존 `if not panes_list:` 조건부는 agent 가 하나라도 있으면 라벨 전용 패인을 통째로 감췄다 — H-16/H-18/H-23 이 검증하려는 시나리오 자체를 표현할 수 없었다. |
| `save_state` 가 동일 `pane_id` 를 append 대신 **in-place 갱신** | ✅ 버그 수정. 기존 로직은 갱신을 무시(첫 항목 고정)했다. 기존 병렬 상태 경합 테스트(10-agent)도 여전히 통과한다. |
| `monkeypatch.delenv("HERDR_WORKSPACE_ID")` | ✅ 필수. 호스트 환경 오염으로 인한 위양성/위음성 차단. |

**유실 검토**: `agent send` 제거는 프로덕션 코드에서 해당 호출이 완전히 사라진 뒤에 이뤄졌으며(`grep` 결과 잔존 0건), 412 테스트 전량 통과가 이를 뒷받침한다. 기능 유실 없음.

---

## 4. 비차단 후속 과제 (Non-blocking / 관찰 사항)

머지를 막지 않으며, 별도 티켓으로 추적할 것을 권고한다.

### N-1 (Low) — 영속 파일은 herdr 워크스페이스를 1개만 기억한다
`_herdr_persist_ws_id` 는 `new-session` 마다 파일을 덮어쓴다. 레이아웃 오버플로(W2b)로 하나의 MAM 워크스페이스가 herdr 워크스페이스 2개 이상을 소유하게 되면, 파일은 **마지막 것만** 가리킨다. 이 경우 `_herdr_ws_scope` 를 쓰는 pane list 폴백은 앞선 워크스페이스의 **라벨 전용 패인**을 찾지 못할 수 있다.
- 완화 요인: `agent get` 경로(env-only 스코프)가 먼저 시도되므로 **정상 등록된 에이전트는 영향받지 않는다.** 영향 범위는 "다중 herdr 워크스페이스 + 라벨 전용 패인" 교집합으로 좁다.
- 구현자가 이 트레이드오프를 `_herdr_agent_get_scoped` 주석에 명시적으로 문서화한 점은 적절하다.
- 권고: 향후 단일 id 대신 **id 목록**(append + dedupe)으로 확장.

### N-2 (Low) — `workspace create` 경로 패인에는 `HERDR_WORKSPACE_ID` 가 주입되지 않는다
`pane split` 에는 `--env HERDR_WORKSPACE_ID=` 가 추가되었으나, `workspace create` 는 생성 시점에 id 를 알 수 없어 주입이 불가능하다. 해당 패인에서 실행되는 에이전트는 env 없이 **파일 스코프에만** 의존한다.
- `WORKSPACE_ROOT` 가 MAM 워크스페이스 단위이므로 일반적인 경우 올바르게 동작한다. 다만 N-1 과 결합하면 스코프가 흔들릴 수 있다.
- 권고: `workspace create` 직후 `pane set-env`(지원 시)로 사후 주입.

### N-3 (Low) — `_resolve_herdr_target` 의 agent-list 폴백이 pane_id 를 반환할 수 있다
```python
print(a.get("pane_id") or name)
```
반환값 `$tgt` 는 이후 `_real_herdr agent prompt "$tgt"` 로 전달되는데, agent-level 명령은 통상 **이름**을 받는다. pane_id 가 반환되면 그 호출이 실패할 수 있다.
- **본 변경분이 도입한 결함이 아니다** — 변경 전 코드도 `print(pane_id or agent)` 로 동일했다(기존 동작 보존). 또한 그 앞의 `agent get` 2단이 성공하는 정상 경로에서는 도달하지 않는다.
- 권고: `name` 을 반환하도록 정리.

### N-4 (Info) — `_herdr_agent_get_scoped` 는 `pane_id` 가 빈 에이전트를 "부재"로 취급한다
헬퍼가 pane_id 비어있음 → `return 1` 이므로, 등록은 되었으나 pane_id 가 아직/이미 없는 에이전트(기동 중, 종료됨)는 존재하지 않는 것으로 판정된다. `HERDR_WORKSPACE_ID` 가 설정된 상태에서는 `agent prompt` 가 `exit 1` 로 끝난다.
- 실무상 pane 없는 에이전트에 프롬프트를 넣는 것은 어차피 무의미하므로 **현재로선 안전한 방향의 실패(fail-safe)** 이다. 동작 변화로 기록만 해 둔다.

### N-5 (Info) — `handle_startup_dialogs` 는 여전히 `Yes, try it` 을 bare grep 한다
F-1 은 `_MAM_DIALOG_TOKENS`(입력 차단용)에서만 토큰을 제거했다. `handle_startup_dialogs` 는 기동 창(기본 20s) 동안 산문에 같은 문자열이 있으면 Escape 를 보낼 수 있다.
- 영향: 기동 직후 유휴 프롬프트에 Escape 1회 → 실질 무해. 또한 이 창은 에이전트가 아직 대화를 시작하기 전이라 산문 노출 확률이 매우 낮다.
- 의도적 설계이며 테스트(`test_fullscreen_modal_is_rejected_not_accepted`)로 정탐이 고정되어 있다.

### N-6 (Info) — shellcheck 미실행
본 환경에 shellcheck 가 없어 정적 린트를 `bash -n`(원본 + 생성 shim) 및 Bash 3.2 실측으로 대체했다. CI 게이트가 아니므로 차단하지 않으나, 향후 CI 에 shellcheck 를 추가하면 `$env_flags` 무인용 확장(SC2086) 등 기존 관용 패턴에 대한 명시적 예외 선언을 함께 정리할 수 있다.

---

## 5. 결론

- 계획서 `plan-fae58b93.md` 의 **F-1 / F-2b / F-3 / F-4 및 워크스페이스 세션 격리(ISSUE-1/2/3/5)** 가 코드에 빠짐없이 반영되었음을 diff 전량 판독으로 확인했다.
- 각 수정에 대해 **동작 검증형 회귀 테스트**(mock RPC 호출 로그 기반)와 **소스 레벨 회귀 방지 테스트**(옛 패턴 잔존 금지)가 쌍으로 추가되어, 향후 되돌림에 대한 방어가 이중으로 걸려 있다.
- 테스트 하네스 변경은 통과를 위한 완화가 아니라 **실제 herdr CLI 계약 쪽으로의 교정**이며, 오히려 기존 mock 이 은폐하던 결함(존재하지 않는 `agent send` 의 무조건 성공)을 드러내는 방향이다.
- 리뷰어 환경에서 **`412 passed`(exit 0)** 를 독립적으로 재현했다.
- 잔여 항목(N-1 ~ N-6)은 전부 저위험 관찰 사항이며, 설계 변경이나 재작업 수준의 재계획을 요구하지 않는다. 따라서 `[ESCALATE: PLANNER]` 는 발행하지 않는다.

**최종 승인한다.**

[VERDICT: PASS]
