# B-3 구현 계획서 **Rev.2** — herdr 프리플라이트가 실제 바이너리를 검사하도록 수정

- **Job**: `9840cee7` / Role: **Planner** (Rev.1 = Job `1876c7c1`)
- **대상**: IMPROVEMENTS.md **B-3** — "`command -v herdr` 프리플라이트 무력화"
- **기준 커밋**: `924e77e` (clean tree)
- **반영**: Creator `agy` Challenge Report (Job `a56c0c5f`) — 심링크 맹점 1건
- **검증 상태**: Rev.1 + Rev.2 수정 전량을 `git archive HEAD` 클론 3벌(HEAD / Rev.1 / Rev.2)에 **실제 구현하여 재측정 완료**.

---

## 0. Rev.1 → Rev.2 변경 요지

| 이의 | 판정 | 근거 |
|---|---|---|
| **제시된 기제** — macOS `/var`↔`/private/var`, `/tmp`↔`/private/tmp`, `~/projects`→`/Volumes/...` 심링크로 인해 `.mam/shim` **디렉터리** 패턴 매칭이 우회됨 | **기각 (REFUTED)** | 인용된 심링크 형태 **전부 이미 SKIP** 된다. 구조적 이유: 심링크 해석은 경로의 **접두부**를 바꾸는데 패턴은 **접미부** `/.mam/shim/` 를 본다. 같은 디렉터리의 어떤 표기든 접미부는 동일하므로 정규화가 결과를 바꿀 수 없다. |
| **제시된 처방** — PATH 항목마다 `cd -P`/`pwd -P` 로 canonical 비교 | **기각 (REJECTED)** | 실제로 존재하는 유일한 상대경로 구멍(`.mam/shim`)조차 **cwd 가 `WORKSPACE_ROOT` 와 같을 때만** 닫는다. 다르면 상대 `cd -P` 가 실패해 구멍이 그대로 열린다(실측). 게다가 PATH 항목마다 서브셸을 띄운다. |
| **결론** — "심링크로 shim 가드를 우회할 수 있다" | **✅ 인용 (CORRECT)** | **맞다.** 다만 기제가 디렉터리가 아니라 **파일**이다. 평범한 bin 디렉터리 안의 심링크(`/usr/local/bin/herdr → <ws>/.mam/shim/herdr`)는 **어떤 디렉터리명 패턴으로도 걸러낼 수 없고**, Rev.1 이 이를 실제 바이너리로 오판한다(실측 확인). |
| **(부수) 자체 발견** | 수정 | 상대 PATH 항목 `.mam/shim`(선행 `/` 나 `./` 없음)이 Rev.1 패턴을 통과한다. |

**총평**: `agy` 의 **결론은 옳았고 분석은 틀렸다.** 지적한 세 가지 심링크 시나리오는 모두 이미 처리되고 있었으며 제안 코드는 그들이 상정한 문제도, 실재하는 문제도 해결하지 못한다. 그러나 "심링크로 우회 가능"이라는 **판단 자체는 참**이었고, 그 참인 판단을 검증하는 과정에서 **Rev.1 의 실제 결함 2건**을 찾아냈다. 이 지적이 없었다면 두 구멍은 남았을 것이다.

---

## 1. 요약

B-3 은 실재하며 **추적 항목이 서술한 것보다 나쁘다.**

- 원인이 1개가 아니라 **독립적인 우회 경로 2개**다.
- 결과가 "무조건 통과"가 아니라 **행(hang)** 이다 — 실측에서 120초 후 SIGKILL 로만 종료됐다.

`command -v` 대신 `type -P` 를 쓰는 자연스러운 수정은 **작동하지 않는다.** `type -P herdr` 는 `.mam/shim/herdr` 래퍼를 찾아내기 때문이다. HEAD 의 프리플라이트 줄에는 이미 `type -P` 가 있으나 `||` 체인 앞단에서 단락되어 아무 일도 하지 않는다.

**해결**: lib.sh 의 스텁 `_resolve_real_herdr_path()` 를 구현하고 `has_real_herdr()` 술어를 노출한다. 🆕 Rev.2 에서 **문자열 패턴만으로는 불충분**함이 드러나, 후보 파일에 한해 심링크를 해석하는 단계를 추가했다(§2-a).

---

## 2. 현황 측정

### 2-a. 🆕 심링크 우회는 **파일 수준**에서만 발생한다 (Rev.2 핵심)

디렉터리 표기 12종을 Rev.1 패턴에 통과시킨 실측:

```
  /Users/x/proj/.mam/shim                        SKIP
  /var/folders/q_/abc/T/proj/.mam/shim           SKIP     ← agy 시나리오 1
  /private/var/folders/q_/abc/T/proj/.mam/shim   SKIP     ← agy 시나리오 1 (정규화 표기)
  /tmp/proj/.mam/shim                            SKIP     ← agy 시나리오 1
  /private/tmp/proj/.mam/shim                    SKIP     ← agy 시나리오 1 (정규화 표기)
  /Volumes/Data/mam/.mam/shim                    SKIP     ← agy 시나리오 2
  ./.mam/shim                                    SKIP     ← agy 시나리오 3 (상대경로)
  .mam/shim                                      KEEP  ←── 실재하는 구멍 (agy 미지적)
  /usr/local/bin                                 KEEP  (정상)
```

**이의제기가 든 심링크 사례는 전부 이미 막혀 있다.** 이유는 우연이 아니다 — 심링크 해석은 경로 **접두부**를 바꾸고, 패턴은 **접미부** `/.mam/shim/` 를 검사한다. 같은 디렉터리를 가리키는 어떤 표기든 `.../.mam/shim` 으로 끝나므로 양쪽 표기가 동일하게 매칭된다. **canonical 화는 이 판정을 바꿀 수 없다.**

그러나 **파일 수준 심링크는 다르다.** 평범한 bin 디렉터리 안의 링크는 디렉터리명이 아무 패턴에도 걸리지 않는다:

```
$ ln -s <ws>/.mam/shim/herdr  <ws>/symbin/herdr
$ PATH=<ws>/symbin:... ; has_real_herdr
  Rev.1 -> ACCEPTED   <-- shim 래퍼를 진짜 herdr 로 오판
  resolved: <ws>/symbin/herdr
  (readlink -> <ws>/.mam/shim/herdr,  inside .mam/shim: True)
```

**이것이 B-3 결함의 재발 경로다.** 오판된 "실제 herdr" 는 shim 이므로 §2-d 의 행(hang)이 그대로 재현된다.

### 2-b. 우회 경로 2개 (Rev.1 에서 확립, 변경 없음)

herdr 가 전혀 없는 PATH 에서 lib.sh 를 source 한 뒤:
```
command -v herdr  -> [herdr]                 rc=0   ← 우회 ①: lib.sh:498 herdr() 함수
type -t herdr     -> [function]
type -P herdr     -> [<WS>/.mam/shim/herdr]  rc=0   ← 우회 ②: shim 래퍼
```
- 우회 ①: `lib.sh:498` 이 `herdr()` 셸 함수를 정의. `command -v` 는 함수를 매칭.
- 우회 ②: `lib.sh:1742` 가 **source 시점에** `_init_herdr_isolation` 을 호출 → `.mam/shim` 을 PATH 맨 앞에 prepend.

### 2-c. HEAD 의 프리플라이트는 "고쳐진 척" 되어 있다

```bash
create_session.sh:84
command -v herdr >/dev/null || type -P herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
```
`git blame`: `ea36e816`. `type -P` 가 `||` 체인 **뒤쪽**이라 앞의 `command -v` 가 rc=0 을 내면 **평가조차 되지 않는다.** 평가되더라도 우회 ②로 통과한다. 실측: `PREFLIGHT: PASS   <-- binary absent, yet passes`.

### 2-d. 실제 결과는 "통과"가 아니라 "행(hang)"

claude 는 있고 herdr 만 없는 상태의 HEAD 실측:
```
subprocess.TimeoutExpired: ... timed out after 120 seconds
<Popen: returncode: -9 ...>          ← SIGKILL 로만 종료
```
프리플라이트 통과 후 shim 을 통해 세션을 만들려다 블록된다. shim 내부 `_resolve_real_herdr` 는 실제 herdr 를 못 찾으면 리터럴 `"herdr"` 로 폴백하고 그 지점에서 멈춘다. **사용자 영향은 "잘못된 성공"이 아니라 "원인 불명의 멈춤"이다.**

### 2-e. 올바른 스캔 로직은 이미 저장소 안에 있다

`lib.sh:53-56` 의 `_resolve_real_herdr_path()` 는 스캔을 전혀 하지 않는 **스텁**이다(C-4 가 미사용 심볼로 추적 중). 반면 **shim 스크립트 내부**에는 `.mam/shim` / `*-shim*` 을 건너뛰는 스캔이 이미 구현되어 있다 → 승격하면 된다. (단 Rev.2 는 여기에 파일 수준 해석을 **추가**한다.)

### 2-f. 기존 테스트가 못 잡은 이유

`conftest.py` 의 `mock_herdr` 픽스처는 항상 실행 가능한 mock 바이너리를 PATH 에 올린다. 즉 **"herdr 가 없는" 경로가 한 번도 실행되지 않았다.**

---

## 3. 변경 명세

### F1 — `.agents/skills/lib.sh` (핵심) 🆕 Rev.2 개정

스텁을 3개 함수로 교체한다.

```bash
# Canonicalise a file path: follow symlinks (bounded), then resolve the parent
# directory with `cd -P`. Hand-rolled rather than shelling out to `readlink -f`
# or `realpath`: both exist on this host and on recent macOS, but neither is
# guaranteed on older macOS or on minimal container images, and this runs inside
# a pre-flight that must not itself depend on an optional tool.
_canonical_file() {
  local p="$1" t d b i=0
  while [ -L "$p" ] && [ "$i" -lt 40 ]; do
    t="$(readlink "$p" 2>/dev/null)" || break
    case "$t" in
      /*) p="$t" ;;
      *)  p="$(dirname "$p")/$t" ;;
    esac
    i=$((i + 1))
  done
  d="$(cd -P "$(dirname "$p")" 2>/dev/null && pwd -P)" || return 1
  b="$(basename "$p")"
  printf '%s/%s\n' "$d" "$b"
}

# True if a path lies inside any wrapper/shim location.
# The argument is wrapped as "/$1/" so the leading-slash patterns match even a
# bare relative entry such as `.mam/shim` (an absolute path just gains a
# harmless leading `//`).
_is_shim_path() {
  case "/$1/" in
    *"/.mam/shim/"*|*-shim/*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*)
      return 0 ;;
  esac
  return 1
}

# Resolve the REAL herdr binary, skipping every wrapper/shim location.
# Prints the absolute path on stdout; returns 1 when only wrappers exist.
#
# B-3: neither of the obvious one-liners can answer "is herdr installed?":
#   * `command -v herdr` matches the herdr() shell FUNCTION defined below.
#   * `type -P herdr` matches $WORKSPACE_ROOT/.mam/shim/herdr, because
#     _init_herdr_isolation prepends that dir to PATH when this file is sourced.
_resolve_real_herdr_path() {
  local dir cand save_ifs="$IFS" real_path=""
  IFS=:
  for dir in $PATH; do
    [ -n "$dir" ] || continue
    # Cheap string reject first: skips wrapper dirs without touching the disk.
    _is_shim_path "$dir" && continue
    [ -x "$dir/herdr" ] || continue
    # Only now — for an actual candidate, so at most once per call — resolve
    # symlinks. A link like /usr/local/bin/herdr -> <ws>/.mam/shim/herdr sits in
    # a directory no string pattern can reject; only the resolved target tells
    # the truth. Falls back to the literal path if canonicalisation fails.
    cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr"
    [ -n "$cand" ] || cand="$dir/herdr"
    _is_shim_path "$cand" && continue
    real_path="$dir/herdr"
    break
  done
  IFS="$save_ifs"
  [ -n "$real_path" ] || return 1
  _REAL_HERDR_PATH="$real_path"
  export _REAL_HERDR_PATH
  printf '%s\n' "$real_path"
}

# Preflight predicate: true iff a real herdr BINARY is installed (not the
# herdr() function, not the .mam/shim wrapper). Use this in every preflight
# instead of `command -v herdr` / `type -P herdr`.
has_real_herdr() {
  _resolve_real_herdr_path >/dev/null 2>&1
}
```

**설계 근거**

| 결정 | 이유 |
|---|---|
| 🆕 **문자열 먼저, canonical 은 후보에만** | 정확성상 필요한 지점은 "받아들이려는 후보"뿐이다. PATH 전체를 canonical 화하는 것은 불필요하고 비싸다(§6-C 실측 **11.07 → 2.70 ms**, 서브셸 20회 → 1회). |
| 🆕 **`"/$dir/"` 정규화** | 선행 슬래시를 강제해 상대 항목 `.mam/shim` 까지 매칭. 절대경로는 무해한 `//` 접두부만 얻는다. 비용 0. |
| 🆕 **`_canonical_file` 을 직접 구현** | `readlink -f`/`realpath` 는 이 호스트와 최신 macOS 에는 있으나(실측 `/usr/bin/readlink`, `/bin/realpath`) 구형 macOS·최소 컨테이너에서는 보장되지 않는다. 프리플라이트가 선택적 도구에 의존해서는 안 된다. |
| **배열 미사용** | 실행 bash 가 3.2.57 이고 `set -u` 이므로 빈 배열 전개가 치명적이다(O-1 라운드 실측). `IFS` 순회 + `case` 만 사용. |
| **해석 깊이 40 제한** | 심링크 순환에서 무한 루프 방지. |
| **canonical 실패 시 리터럴 폴백** | 권한 없는 디렉터리 등에서 `cd -P` 가 실패해도 스캔이 중단되지 않도록. |

**C-4 교차**: `_REAL_HERDR_PATH`, `_HERDR_SHIM_DIR_PATTERN`, `_HERDR_SKILLS_BIN_PATTERN` 3종이 미사용 → 사용 상태로 전환된다. C-4 목록에서 제외 필요(본 계획은 IMPROVEMENTS 를 수정하지 않음 — GM 재량).

### F2 — `create_session.sh:84`

```bash
# before
command -v herdr >/dev/null || type -P herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
# after
# B-3: `command -v herdr` matches lib.sh's herdr() function and `type -P herdr`
# matches the .mam/shim wrapper, so both pass with no herdr installed.
has_real_herdr || { echo "ERROR: herdr not installed" >&2; exit 1; }
```
`lib.sh` 는 22행에서 이미 source 되므로 84행 시점에 정의되어 있다.

### F3 — `multi-agent-mux-delegate-job:448`

```bash
# before
  if ! command -v herdr >/dev/null 2>&1; then
# after
  # B-3: must probe for the real binary, not the herdr() function / shim wrapper.
  if ! has_real_herdr; then
```

### F4 — 문서 2건

`create/SKILL.md:37`, `status/SKILL.md:31` 이 프리플라이트 예제로 `command -v herdr` 를 제시한다. **문서가 결함을 재생산하는 경로**이므로 함께 고친다(부록 B).

### F5 — `tests/test_b3_herdr_preflight.py` (신규 **218줄**, Rev.1 163줄 → +55)

`mock_herdr` 를 **의도적으로 쓰지 않고** `mam_sandbox` 만 사용하며, PATH 를 `BARE_PATH` 로 고정해 개발자 머신의 herdr 가 회귀를 가리지 못하게 한다.

| ID | 검증 대상 |
|---|---|
| W-1 | 우회 2개 특성화 — 미래 리팩터가 one-liner 로 되돌리지 못하게 고정 |
| W-2 | 바이너리 없음 → FALSE |
| W-3 | 바이너리 있음 → TRUE, shim 이 아닌 실제 경로 반환 |
| W-4 | `my-shim` / `multi-agent-herdr-shim` 디렉터리는 불인정 (×2) |
| W-5 | `create_session.sh` rc≠0 **이고** 메시지가 herdr 게이트의 것 |
| W-6 | 실제 바이너리를 거부하지 않음 |
| W-7 | claude 만 있고 herdr 없음 → herdr 게이트에서 정지 |
| 🆕 **W-8** | **평범한 bin 디렉터리 안의 shim 심링크 → 거부** |
| 🆕 **W-9** | **실제 바이너리를 가리키는 심링크 → 수용** (전면 심링크 거부가 아님을 고정) |
| 🆕 **W-10** | **상대 PATH 항목 `.mam/shim` → 거부** |

W-9 가 중요하다. W-8 만 있으면 "심링크는 전부 거부" 라는 잘못된 구현으로도 통과한다. W-9 가 그 오답을 배제한다.

### F6 — `IMPROVEMENTS.md`

게이트 통과 후 B-3 을 §5 완료로 이관, §2 헤더 `8건`→`7건`, §5 `4건`→`5건`, 총계 조정. B-3 본문에 §2-a(파일 수준 심링크)·§2-d(행 발생)를 반영 권고.

---

## 4. 적용 순서

F1 → F2 → F3 → F5 → **red/green 측정** → F4 → **회귀 비교** → F6.

---

## 5. 이의제기 심층 판정

### 5-A. 제시된 기제(디렉터리 심링크) — **기각**

주장: `/var`↔`/private/var` 등 심링크 표기 차이로 `.mam/shim` 디렉터리 패턴이 우회된다.

**반증**: §2-a 표 참조. 인용된 6개 심링크 형태가 **전부 SKIP** 된다. 구조적 이유가 있다 — 심링크 해석은 **접두부**를 바꾸고 패턴은 **접미부** `/.mam/shim/` 를 본다. 어떤 표기로 쓰든 그 디렉터리는 `.../.mam/shim` 으로 끝난다. **정규화가 판정을 바꿀 수 없다.**

### 5-B. 제시된 처방(PATH 항목별 canonical 비교) — **기각**

제안 코드는 실재하는 유일한 상대경로 구멍(`.mam/shim`)조차 **조건부로만** 닫는다. 실측:
```
cwd == WORKSPACE_ROOT :  canonical_dir=[<ws>/.mam/shim]  → SKIP (구멍 닫힘)
cwd != WORKSPACE_ROOT :  canonical_dir=[<empty>]         → KEEP (구멍 열린 채)
```
상대 `cd -P ".mam/shim"` 는 cwd 기준이므로 cwd 가 워크스페이스가 아니면 실패하고, 그 경우 비교가 성립하지 않아 통과한다. **cwd 가 `WORKSPACE_ROOT` 와 다른 것은 특수 상황이 아니라 일상적이다.**

더불어 PATH 항목마다 서브셸을 띄운다(§6-C: 20항목 PATH 에서 **11.07 ms/call**, 후보 한정 방식의 **4.1배**). 성능은 부차적 근거이며 **기각의 주된 이유는 정확성**이다.

### 5-C. 결론(심링크로 우회 가능) — **인용**

**옳다.** 다만 기제가 디렉터리가 아니라 **파일**이다. 이의제기 문서의 마지막 권고에 *"실체 파일 경로/inode 를 배제"* 라는 표현이 스쳐 지나가는데, 제안 코드는 그것을 구현하지 않았다(디렉터리만 canonical 화). 실측으로 이 경로가 실재함을 확인하고(§2-a) Rev.2 에서 **후보 파일 canonical 화**로 닫았다.

### 5-D. 자체 발견 — 상대 PATH 항목

Rev.1 검증 중 `.mam/shim`(선행 `/`·`./` 없음)이 패턴을 통과함을 발견했다. `_init_herdr_isolation` 은 절대경로를 넣으므로(`WORKSPACE_ROOT` 기본값이 `pwd` 출력) **이 경로로는 도달 불가**하며, 사람이나 외부 도구가 PATH 에 직접 넣어야 재현된다. 도달성은 낮지만 **비용 0의 한 토큰 수정**(`"$dir/"` → `"/$dir/"`)으로 닫히므로 닫았다. 도달성을 부풀리지 않고 기록한다.

---

## 6. 검증 게이트

### G-A — 신규 테스트 red→green

```
HEAD  (unpatched) : 2 failed, 5 passed in 0.37s   [W-1..W-6]
                    + W-7 별도: FAILED (120s timeout, rc=-9)
Rev.1             : W-8 FAILED, W-10 FAILED, W-9 passed
Rev.2 (전량)      : 11 passed in 0.66s
```

| 테스트 | HEAD | Rev.1 | Rev.2 | red 의 성격 |
|---|---|---|---|---|
| W-1 | PASS | PASS | PASS | 특성화 |
| W-2 | PASS† | PASS | PASS | 공허한 통과 |
| W-3 | FAIL | PASS | PASS | 오염된 red |
| W-4 | PASS† | PASS | PASS | 공허한 통과 |
| W-5 | **FAIL** | PASS | PASS | **진짜 red** |
| W-6 | PASS | PASS | PASS | 회귀 핀 |
| W-7 | **FAIL(hang)** | PASS | PASS | **진짜 red — 가장 강함** |
| 🆕 W-8 | — | **FAIL** | PASS | **진짜 red (Rev.1 대비)** |
| 🆕 W-9 | — | PASS | PASS | 오답 배제 핀 |
| 🆕 W-10 | — | **FAIL** | PASS | **진짜 red (Rev.1 대비)** |

> **red 의 정직성 — 반드시 함께 읽을 것**
> **†W-2 / W-4** 는 HEAD 에서 `has_real_herdr` 가 **존재하지 않아** command-not-found → 비영 → FALSE 분기를 타서 **공허하게 통과**한다. red 가 아니라 회귀 핀이다.
> **W-3** 은 존재하지 않는 심볼을 호명해 실패하므로 **오염된 red**다.
> **W-5** 는 진짜 red 다. HEAD 에서 rc≠0 이지만 **herdr 게이트가 아니라 다음 claude 게이트에서** 죽는다. 단언을 `rc != 0` 하나로 뒀다면 **HEAD 에서 통과해 결함을 놓쳤을 것**이다.
> **W-7** 이 가장 강하다 — 새 심볼을 일절 호명하지 않고 관측 동작만 검사한다. HEAD 결과는 assertion failure 가 아니라 **120초 타임아웃 + SIGKILL** 이었다.
> **W-8 / W-10** 은 **Rev.1 대비 진짜 red** 다. 이 2건이 이번 이의제기의 실질 산출이다.
> → 누적 **진짜 red 4건**: W-5·W-7(HEAD 대비), W-8·W-10(Rev.1 대비).

### G-B — 회귀 위험의 직접 측정

이 변경의 유일한 실질 회귀 위험은 **정당한 herdr 를 거부하는 것**이다.
```
mock_herdr 픽스처와 동일 레이아웃(<tmp>/bin/herdr) : ACCEPTED
실제 바이너리를 가리키는 심링크                    : ACCEPTED   (W-9)
bash 3.2 + set -euo pipefail 로 has_real_herdr     : OK (rc=0)
```

### G-C — 🆕 비용 실측 (제안 방식 대비)

20항목 PATH, herdr 는 마지막 디렉터리(양쪽 최악 조건):
```
  PATH 항목마다 canonical (agy 제안) : 11.07 ms/call   서브셸 20회
  후보에만 canonical    (Rev.2)      :  2.70 ms/call   서브셸  1회
```
4.1배 차이. **다만 기각 사유의 주된 근거는 §5-B 의 정확성 문제이며 성능은 부차적이다.**

### G-D — 회귀 비교 (동일 스위트, 두 클론)

`create_session.sh` 를 구동하는 `tests/test_sanity.py` 를 patched / unpatched 동일 조건 실행:
```
PATCHED : 2 failed in 194.52s    HEAD : 2 failed in 194.71s
  FAILED test_create_session_dry_run  - assert '[dry-run] ...      (양쪽 동일)
  FAILED test_create_session_full     - KeyError: 'isolation'      (양쪽 동일)
```
실패 집합·오류 메시지·소요 시간이 동일 → **선행 결함이며 본 변경이 유발한 회귀는 0건**이다. (`KeyError: 'isolation'` 은 C-3 영역으로 보이나 본 계획 범위 밖이며 수정하지 않는다.)

> **전체 스위트(`pytest tests/`)는 실행하지 않았다.** 본 과제의 앞선 두 위임(`c3763464`, `a5639650`)이 정확히 이 시도에서 예산을 소진했다 — 호스트에서 실 오케스트레이션 루프가 동시 구동 중이라 pytest 가 반복적으로 SIGKILL(exit 137/144)됐다. 대신 **변경이 실제로 닿는 경로**를 두 클론에서 동일 비교하는 방식으로 판정했고, 이 비교는 절대 통과율과 무관하게 "내 변경이 유발했는가"에 결정적으로 답한다. Creator 는 한가한 시점에 `pytest tests/` 1회 완주를 권고한다.

### G-E — 정적 검사

```
bash -n lib.sh / create_session.sh / multi-agent-mux-delegate-job  → syntax OK (3 files)
잔여 오지침(`command -v herdr` / `type -P herdr`)                   → 설명 주석 2건만 잔존 (지침 아님)
```

### 측정된 변경 규모 (Rev.2)

```
  -2   +68   .agents/skills/lib.sh                                        (Rev.1 +34 → +68)
  -1   +3    .agents/skills/multi-agent-mux-create/scripts/create_session.sh
  -1   +2    .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job
  -1   +4    .agents/skills/multi-agent-mux-create/SKILL.md
  -1   +5    .agents/skills/multi-agent-mux-status/SKILL.md
  new  218   tests/test_b3_herdr_preflight.py                             (Rev.1 163줄 → +55)
```

---

## 7. 리스크

| ID | 리스크 | 평가 / 완화 |
|---|---|---|
| **RK-A** | 정당한 herdr 설치를 거부 | G-B 로 직접 측정 — 픽스처 레이아웃·실 바이너리 심링크 모두 수용. W-3/W-6/W-9 가 핀. |
| **RK-B** | 정상 디렉터리명에 `-shim` 이 우연히 포함 | 해당 herdr 는 무시된다. 의도된 동작(shim 을 실제로 오인하는 편이 더 위험)이며 W-4 가 명시. `_HERDR_SHIM_DIR_PATTERN` 으로 조정 가능. |
| **RK-C** | bash 3.2 + `set -u` 이식성 | 배열 미사용 구현으로 회피. **G-B 에서 `/bin/bash` 3.2.57 + `set -euo pipefail` 직접 실행 확인.** |
| **RK-D** | PATH 빈 항목(`::`)이 `.` 로 해석 | `[ -n "$dir" ] \|\| continue` 로 방어. |
| **RK-E** 🆕 | `_canonical_file` 이 `cd -P` 실패 시 오작동 | 리터럴 경로로 폴백해 스캔을 계속한다. 최악의 경우 Rev.1 수준(문자열 패턴만)으로 **안전 퇴화**하며, 더 나빠지지 않는다. |
| **RK-F** 🆕 | 심링크 순환 | 해석 깊이 40 상한. 초과 시 마지막 경로로 판정 — 폴백과 동일하게 안전 퇴화. |
| **RK-G** 🆕 | 후보 canonical 화 비용 | 호출당 최대 1회 서브셸(G-C 실측 2.70 ms). 프리플라이트는 세션 생성/위임 시 1회만 호출되므로 무시 가능. |
| **RK-H** | C-4 목록이 낡음 | 심볼 3종이 사용 상태로 전환. C-4 에서 제외 필요 — GM 재량. |
| **RK-I** | 문서와 코드가 따로 감 | F2/F3(코드)와 F4(문서)를 한 커밋으로 묶을 것을 권고. 문서가 `command -v` 를 계속 가르치면 결함이 재도입된다. |
| **RK-J** | `test_sanity.py` 선행 실패 2건이 본 변경 탓으로 오인 | G-D 가 양쪽 클론 동일 실패를 명시. 리뷰 시 이 표를 근거로 제시할 것. |

---

## 8. 역할 경계

본 문서는 **설계 산출물**이며 저장소 코드·문서는 **한 줄도 수정하지 않았다**. 모든 구현·측정은 세션 스크래치패드 내 `git archive HEAD` 클론 3벌(HEAD / Rev.1 / Rev.2)에서만 수행했고, 작업 트리는 `924e77e` 기준 clean 을 유지한다.

`MULTI_AGENT_RULES.md` §1 에 따라 **구현은 Creator, 커밋은 GM 소관**이다. **차단 항목 없음.**

---

## 부록 A — 신규 테스트 3종 전문 (W-8 / W-9 / W-10)

Rev.1 의 W-1…W-7 은 그대로 유지된다. 아래는 Rev.2 추가분이다.

```python
# W-8 — symlink bypass: a link in an ORDINARY bin dir pointing at the shim.
#       No directory-name pattern can reject this; only the resolved target
#       reveals it.
def test_b3_symlink_into_shim_is_rejected(mam_sandbox):
    _bash(mam_sandbox, "true")                 # materialise the shim
    shim = mam_sandbox / ".mam" / "shim" / "herdr"
    assert shim.exists(), "shim wrapper was not created by _init_herdr_isolation"

    linkdir = mam_sandbox / "usrlocalbin"
    linkdir.mkdir(parents=True, exist_ok=True)
    (linkdir / "herdr").symlink_to(shim)

    res = _bash(mam_sandbox,
                'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
                path=f"{linkdir}:{BARE_PATH}")
    assert "RESULT:FALSE" in res.stdout, (
        "a symlink pointing into .mam/shim was accepted as a real herdr:\n"
        f"{res.stdout}\n{res.stderr}")


# W-9 — ... but a symlink to a REAL binary must still be accepted, so the
#       canonicalisation cannot be a blanket "reject all symlinks".
def test_b3_symlink_to_real_binary_is_accepted(mam_sandbox):
    real = _make_herdr(mam_sandbox / "realbin")
    linkdir = mam_sandbox / "linkbin"
    linkdir.mkdir(parents=True, exist_ok=True)
    (linkdir / "herdr").symlink_to(real)

    res = _bash(mam_sandbox,
                'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
                path=f"{linkdir}:{BARE_PATH}")
    assert "RESULT:TRUE" in res.stdout, \
        f"a symlink to a genuine herdr was rejected:\n{res.stdout}\n{res.stderr}"


# W-10 — a bare relative PATH entry '.mam/shim' (no leading '/' or './') must
#        still be rejected. Requires normalising the dir with a leading slash.
def test_b3_bare_relative_shim_entry_is_rejected(mam_sandbox):
    _bash(mam_sandbox, "true")                 # materialise the shim
    res = _bash(mam_sandbox,
                'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
                path=f".mam/shim:{BARE_PATH}")
    assert "RESULT:FALSE" in res.stdout, (
        "a bare relative '.mam/shim' PATH entry was accepted:\n"
        f"{res.stdout}\n{res.stderr}")
```

Rev.2 해석기에 대한 전 시나리오 실측:
```
  real binary          : ACCEPT  <ws>/realbin/herdr
  symlink -> real      : ACCEPT  <ws>/goodlink/herdr
  symlink -> shim      : reject
  2-hop chain -> shim  : reject
  shim dir directly    : reject
  wrapper dir (my-shim): reject
  nothing installed    : reject
```

---

## 부록 B — 문서 수정 전문 (F4)

**`create/SKILL.md`**
```bash
# 1) herdr available and isolated server status
# Use lib.sh's has_real_herdr, NOT `command -v herdr` / `type -P herdr`: once
# lib.sh is sourced the former matches its herdr() function and the latter
# matches the .mam/shim wrapper, so both pass on a host with no herdr (B-3).
has_real_herdr || { echo "ERROR: herdr not installed"; exit 1; }
```

**`status/SKILL.md`**
````markdown
```bash
has_real_herdr || { echo "ERROR: herdr not installed"; exit 1; }   # lib.sh helper (B-3)
command -v python3
test -f .mam/agent-sessions.yaml
```

> `command -v herdr` and `type -P herdr` both report success even when herdr is
> not installed — they match lib.sh's `herdr()` function and the `.mam/shim`
> wrapper respectively. Always use `has_real_herdr`.
````

---

[AGREEMENT: REACHED]
