# 📐 구현 계획서 **Rev.2** — Job `d6f54b7f` (원안: `79ff98ed`)

- **역할**: Planner (`MULTI_AGENT_RULES.md` §1 — 저장소 코드/문서 무수정, 산출물은 본 보고서)
- **기준 커밋**: `14e306b` (branch `refactor`, working tree clean)
- **베이스라인**: `pytest tests/ --collect-only` → **333 collected**
- **입력**: Job `9f85218e` 리뷰 `[VERDICT: PASS WITH CHALLENGE]` (Challenge C-1, Observation C-2)

---

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

| 항목 | 판정 | 조치 |
|---|---|---|
| **Challenge C-1** — T5 문서 가드의 블록 카운팅 오류 + 부분 문자열 허점 | **전면 수용. 두 갈래 모두 실측 확인** | §5 T5 재설계 (§1.10에 실측 근거) |
| **Observation C-2** — `_env_int` 의 `ValueError` 시 `continue` | **수용. 챌린저가 제시한 것보다 근거가 더 강함** | §4.5 S5 변경 + 전용 테스트 T1b 신설 (§1.11) |
| (자체 정정) Rev.1 §5 의 "신설 8건" | **오산 — 실제 7건** | Rev.2 는 8건(C-2 테스트 1건 추가). 기대 collected 341 은 동일하나 근거가 달라짐 |
| (신규) K-5 | — | 문서화되지 않은 `MAM_MIN_COLS` 가 문서화된 `MAM_MIN_PANE_COLS` 보다 **우선순위가 높다** (§9) |

C-1 은 계획대로 구현하면 **테스트가 100% 실패**하는 결함이었습니다. 챌린저의 지적이 정확했고, 실측으로 재현했습니다(§1.10). 다만 챌린저가 제시한 수정안은 **다른 실패 모드를 새로 만듭니다** — 문서 전체를 스캔하므로 산문 속 파일명 언급을 명령으로 오인합니다. 그래서 **커맨드 단위 검증(챌린저의 핵심 교정)** 에 **펜스 스코프(추가 보강)** 를 합성했습니다. §1.10.3 에 두 실패 모드를 각각 실측했습니다.

C-2 는 챌린저가 "다중 fallback 취지에 부합" 정도로 완곡하게 제기했지만, 실측해 보니 **`.mam.env.example` 이 문서화한 유일한 이름이 조용히 무시되는** 경로였습니다. 근거를 강화해 수용합니다(§1.11).

---

## 1. 실측 (Measurements)

> §1.1 ~ §1.9 는 Rev.1 에서 확정된 실측이며 재검증 없이 유지합니다. §1.10 · §1.11 이 Rev.2 신규입니다.

### 1.1 에이전트 해석기가 저장소에 **4개** 존재한다

| # | 위치 | 우선순위 | 실패 시 |
|---|---|---|---|
| **1** | `lib_py/agents/registry.py:26` `agent_of_row()` | `agent` 필드 → 이름 접미사 → `pane.cmd` | `None` |
| **2** | `stop_session.sh:101-109` | 이름 접미사만 (역할 한정) | `exit 2` |
| **3** | `update_yaml_resumed.sh:44-51` | **#2 와 완전 동일한 복사본** | `exit 2` |
| **4** | `run_loop.sh:278` | `agent` 필드 → `pane.cmd` → 하이픈 세그먼트 → **`claude` 기본값** | 실패 없음 |

```
SESSION_NAME                       stop/upd   run_loop   registry
---------------------------------- ---------- ---------- ----------
x-creator-claude                   claude     claude     claude
agy-creator-01                     EXIT2      agy        None      ← 라이브 세션
my-project-dev-claude              EXIT2      claude     claude    ← INSTALL.md 예제 이름
worker-1-agy                       EXIT2      agy        agy
foo-cline                          EXIT2      cline      cline
bad-session-name                   EXIT2      claude     None      ← run_loop 은 조용히 claude
orc-hermes-main                    EXIT2      hermes     None
```

1. **`agy-creator-01` 은 지금 이 워크스페이스에 running 으로 등록된 실제 세션입니다.** `pane.cmd = 'agy'` 가 기록돼 있는데도 `--agent` 없이는 `exit 2` 로 거부됩니다. 브리프가 지목한 결함의 재현 가능한 구체 사례입니다.
2. `my-project-dev-claude` 는 `deploy/INSTALL.md:95` 가 스스로 문서화한 세션 이름입니다. 접미사가 `-dev-claude` 라 #2 의 역할 한정 케이스에 걸리지 않습니다. INSTALL.md 가 `--agent claude` 를 명시해 사고가 안 났을 뿐입니다.
3. `run_loop.sh` 는 해석 실패를 `claude` 로 흡수합니다. 호출 12곳이라 이번 범위 밖(§9 K-1).

라이브 3개 행에 `agent_of_row` 직접 적용:

```
canary-projects-multi-agent-mux-creator-claude     agent_of_row='claude'  match_cmd=False → 'claude'
canary-projects-multi-agent-mux-creator-cline      agent_of_row='cline'   match_cmd=False → 'cline'
agy-creator-01                                     agent_of_row='agy'     match_cmd=False → None
```

`match_cmd=True` 는 docstring 상 **비-입양(non-adoption) 조회**용이고 `stop`/`update_yaml_resumed` 가 정확히 그 경우입니다. (`reconcile.sh` 입양 루프 금지라는 `3aee63cf` §1.2 반증은 유효하며, 이 계획은 `reconcile.sh` 를 건드리지 않습니다.)

### 1.2 `agent` 필드는 존재하지 않는다

```
row keys 합집합:
['agy_conversation_id_own', 'attach_command', 'child_pid', 'claude_session_id_own',
 'cline_conversation_id_own', 'delegate_job_id', 'herdr_server', 'herdr_session',
 'herdr_session_created_at', 'herdr_session_epoch', 'kill_command',
 'last_visible_status', 'last_visible_status_at_termination', 'mcp_attachments',
 'name', 'pane', 'role', 'start_command', 'status', 'tui']
```

3개 행 전부 `agent=None`, `pane.cmd` 는 3개 전부 채워짐. → `agent` 필드를 **쓰는** 코드는 추가하지 않고, 우선순위 ①은 테스트로만 고정합니다(T4b).

### 1.3 `load_state_json` 은 YAML 이 아니라 SQLite 를 읽는다

`lib.sh:938-978` — `.db` 우선, 없을 때만 `.yaml`. 라이브에 `.mam/agent-sessions.db`(40 KiB) 존재. 문서·커밋 메시지에서 "레지스트리" 로 표현합니다.

### 1.4 비용

| 항목 | 실측 |
|---|---|
| `load_state_json` 1회 | ~34 ms |
| `python3` 기동 + `import lib_py.agents.registry` | ~27 ms |
| `stop_session.sh` 가 이미 수행하는 `load_state_json` | **2회** (`:97`, `:113`) |

`PYTHONPATH` 는 `lib.sh:25` 가 export 하므로 맨 `python3` 로 임포트 가능. venv 없는 시스템 파이썬(3.9.6)에서 `env -i` 검증 완료. `registry` 는 서드파티 의존 없음(`yaml` 불필요 — 상태는 JSON 으로 env 전달).

### 1.5 J-1 재현

페이로드: 1패널 `width=50, height=30`

| 경로 | 결과 |
|---|---|
| `--min-cols 0` | `right` / `single_pane_height_constrained` |
| `MAM_MIN_PANE_COLS=0` | **`overflow`** |
| `MAM_MIN_COLS=0` | **`overflow`** |
| `--min-rows 0` | `down` |
| `MAM_MIN_PANE_ROWS=0` | **`overflow`** |
| **대조군** `--min-cols 25` vs `MAM_MIN_PANE_COLS=25` | **양쪽 동일** (`right`) |

대조군이 결함을 `or` 관용구의 falsy-zero 하나로 국소화합니다.

### 1.6 J-2 임계값

```
n=3 n//2=1 -> down      ← 현행 d3 단언. 상한 검사 도달 불가
n=4 n//2=2 -> overflow
n=5 n//2=2 -> down      ← 판별 가능한 최소 홀수
n=6 n//2=3 -> overflow
n=7 n//2=3 -> down
```

### 1.7 문서 실태

| 파일 | 현상 |
|---|---|
| `multi-agent-mux-stop/SKILL.md` | `--agent` **0회**. 워크플로 예제 3개(`:68, :72, :77`) 전부 생략 |
| `deploy/INSTALL.md:94, :98` | `--agent claude` **이미 명시** — 유일한 모범 사례 |
| `multi-agent-mux-create/SKILL.md:146` | `AGENT=claude  # or agy` |
| `multi-agent-mux-create/SKILL.md:171` | `must be claude or agy` — 실물 `create_session.sh:86` 은 4종을 받음 |
| `multi-agent-mux-resume/SKILL.md:61` | `# or agy or hermes` (cline 누락) |
| `create_session.sh:4`, `resolve_session_id.sh:4` | 헤더 주석 `<claude\|agy>` |
| `update_yaml_resumed.sh:7, :14` | `[--agent claude\|agy]` |

`create_session.sh` 는 이미 `--agent` 필수 + 4종 검증(`:83`, `:85-86`). create 쪽은 **문서 동기화뿐**입니다.

### 1.8 기존 테스트 계약

| 테스트 | 세션명 | 현행 |
|---|---|---|
| `tests/test_tier1_unit.py:142` | `bad-session-name` | rc=2, `cannot infer agent` |
| `tests/test_tier3_integration.py:398` | `bad-name` | rc=2, `cannot infer agent` |

두 이름 모두 샌드박스 레지스트리(`herdr_sessions: []`)에 없습니다. §3 설계 결정을 지배합니다.

### 1.9 (부수) `cd … 2>/dev/null || pwd` 결함

```
line35 result: [/lib.sh]        → 존재하지 않음, 항상 :36 폴백
correct form : [/Users/.../.agents/skills/lib.sh]
```

잔존: `stop_session.sh:35`, `create_session.sh:23`, `resume_session.sh:6`. (`update_yaml_resumed.sh:10` 은 이미 정상.) `31b2d70` 의 R-2 와 동일 결함. 1차 소싱 경로가 100% 죽어 `${WORKSPACE_ROOT:-$PWD}` 폴백에만 의존합니다.

---

### 1.10 **[Rev.2 신규] Challenge C-1 검증**

#### 1.10.1 갈래 ① — `checked == 2` 로 단언이 실패한다 → **확인**

Rev.1 T5 의 블록 단위 정규식을 현재 문서에 그대로 적용:

```
SKILL.md:   total fenced bash/sh blocks=3, containing stop_session.sh=1
   -> one block holds 3 stop_session.sh invocations; '--agent' present in block: False
INSTALL.md: total fenced bash/sh blocks=7, containing stop_session.sh=1
   -> one block holds 2 stop_session.sh invocations; '--agent' present in block: True
CHECKED = 2   (planner asserted >= 4)
```

`assert checked >= 4` 는 **결정론적으로 실패**합니다. 챌린저의 지적이 정확합니다. 제가 §1.7 에서 "예제 3개(`:68, :72, :77`)" 를 세면서도 그것이 **하나의 펜스 안에 들어 있다**는 사실을 확인하지 않은 것이 원인입니다 — 개수는 셌지만 **경계를 세지 않았습니다**.

#### 1.10.2 갈래 ② — 블록 단위 단언의 위양성(False Positive) → **확인**

INSTALL.md 사본에서 **두 호출 중 하나에서만** `--agent` 를 제거하는 뮤테이션:

```
mutation applied (agent count 2 -> 1)
설계 A (블록 단위, Rev.1 원안):     blocks=1  all pass? True    ← 뮤테이션 미검출
설계 C (펜스+커맨드, Rev.2 정제안): checked=2 missing=1        ← 뮤테이션 검출
```

블록에 `--agent` 가 **한 번이라도** 나오면 통과합니다. 회귀를 못 잡는 가드는 가드가 아니라 주석입니다. 챌린저의 지적이 정확합니다.

#### 1.10.3 챌린저 수정안의 잔여 실패 모드 → **문서 전체 스캔이 산문을 명령으로 오인한다**

챌린저 수정안은 `doc.read_text()` **전체**에 커맨드 정규식을 돌립니다. 산문 속 파일명 언급이 있는 문서로 실측:

```
=== 챌린저 수정안 (문서 전체 스캔) ===
  [1] --agent=NO  | '`stop_session.sh` does not delete report trees.'          ← 위양성
  [2] --agent=NO  | '`stop_session.sh --purge-conversation` note below.'       ← 위양성
  [3] --agent=YES | 'bash .../stop_session.sh --session "$S" --agent "$A"'
  [4] --agent=NO  | 'bash .../stop_session.sh --session "$S"'

=== 펜스 스코프 + 커맨드 단위 (Rev.2) ===
  [1] --agent=YES | 'bash .../stop_session.sh --session "$S" --agent "$A"'
  [2] --agent=NO  | 'bash .../stop_session.sh --session "$S"'
  checked=2
```

산문 두 줄이 각각 `checked += 1` 되고 `--agent` 가 없으므로 **테스트가 실패**합니다. 이것이 가설이 아니라 임박한 문제인 이유:

- 이 계획 **§4.4 자체가 stop/SKILL.md 에 산문 문단을 추가**합니다.
- `stop/SKILL.md` 의 `## Pitfalls` · `## When NOT to use` 절은 성격상 스크립트를 산문으로 언급하게 되는 자리입니다.
- 문장을 하나 썼다고 실패하는 가드는 다음 사람이 **지웁니다**.

현재 두 문서에는 펜스 밖 언급이 0건이라(SKILL.md 3회·INSTALL.md 2회 모두 펜스 안) 챌린저 수정안도 **지금은** 통과합니다. 하지만 가드의 존재 이유는 미래의 편집을 견디는 것이므로, 지금 통과하는 것만으로는 부족합니다.

#### 1.10.4 정제안 검증 — 계획 §4.4 적용 후

`§4.4` 대로 편집한 사본(3개 예제에 `--agent "$AGENT"` 추가 + `stop_session.sh` 문자열을 포함하지 않는 산문 문단 추가)에 정제안 적용:

```
SKILL.md     checked=3  missing_agent=0
INSTALL.md   checked=2  missing_agent=0
```

총 5건, 전건 통과. 펜스 스코프 덕분에 **"산문에 파일명을 쓰지 말라"는 제약이 계획에서 사라집니다** — 이것이 챌린저 수정안 대비 실질 이득입니다.

### 1.11 **[Rev.2 신규] Observation C-2 검증 — 근거는 챌린저가 제시한 것보다 강하다**

#### 1.11.1 어느 이름이 정본인가

```
.mam.env.example:133   # MAM_MIN_PANE_COLS=60
.mam.env.example:137   # MAM_MIN_PANE_ROWS=20
.mam.env.example:143   # MAM_MAX_PANE_COLS=3
lib.sh:432             --min-cols "${MAM_MIN_PANE_COLS:-60}" --min-rows "${MAM_MIN_PANE_ROWS:-20}"
test_herdr_shim_contract.py:100-101   export MAM_MIN_PANE_COLS=60 / MAM_MIN_PANE_ROWS=20
```

`MAM_MIN_COLS` / `MAM_MIN_ROWS` / `MAM_MAX_COLS` 단축형은 **`layout.py:191-193` 안에서만** 등장합니다. 생산 코드·문서·템플릿·테스트 어디에도 없습니다. `report-8f0cb35f.md:72` 는 정리 작업 당시 *"no legacy `MAM_MIN_COLS=`/`MAM_MIN_ROWS=` env-prefix style"* 을 확인 사항으로 적고 있습니다 — 단축형은 **레거시 별칭**입니다.

그런데 `_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS")` 는 **레거시 단축형을 먼저** 봅니다.

#### 1.11.2 결과: 문서화된 유일한 이름이 조용히 무시된다

```
env                                                현행 or 60   return default   continue
{}                                                        60               60         60
{'MAM_MIN_PANE_COLS': '0'}                                60                0          0
{'MAM_MIN_PANE_COLS': '25'}                               25               25         25
{'MAM_MIN_COLS': 'foo'}                                   60               60         60
{'MAM_MIN_COLS': '',    'MAM_MIN_PANE_COLS': '25'}        25               25         25
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': '25'}        60               60         25  ← 차이
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': 'bar'}       60               60         60
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': '0'}         60               60          0  ← 차이
```

읽어야 할 두 가지:

1. **`""` 와 `"foo"` 가 다르게 취급됩니다.** 빈 문자열은 다음 후보로 넘어가고(`if raw:` 가 걸러냄), 무효 문자열은 즉시 탈출합니다. 둘 다 "쓸 수 없는 값"인데 처리가 정반대입니다. `continue` 는 이 비대칭을 없앱니다.
2. 차이가 나는 두 행에서 무시되는 값은 **`.mam.env.example` 이 문서화한 바로 그 변수**입니다. 운영자가 템플릿대로 `MAM_MIN_PANE_COLS=25` 를 설정했는데, 셸 어딘가에 남은 `MAM_MIN_COLS=foo` 하나 때문에 60 이 적용됩니다.

3. **차이는 정확히 2행뿐입니다.** 나머지 7행은 세 구현이 완전히 일치합니다. 즉 `continue` 는 J-1 수정과 직교하고, 행동 변경 표면이 "첫 후보 무효 + 후속 후보 유효" 라는 한 조건으로 좁혀집니다. 테스트 1건으로 완전히 고정할 수 있습니다(T1b).

**결론: C-2 수용.** 챌린저는 "다중 fallback 취지에 부합" 이라는 설계 논거로 제기했는데, 실측하면 **문서화된 설정이 무시되는 실동작 결함**이라 근거가 더 강합니다. Rev.1 이 `return default` 를 고른 이유는 "오타 입력에 대한 행동 동등성 보존" 이었고 그 목표 자체는 유효하지만, 위 표의 5·7행이 보여주듯 **`continue` 도 그 목표를 똑같이 만족**합니다(모든 후보가 무효면 `default`). Rev.1 은 더 좁은 불변식을 지키느라 더 나은 것을 놓쳤습니다.

---

## 2. 범위

**포함**

| # | 항목 |
|---|---|
| S1 | `lib.sh` 에 `resolve_agent_type_from_registry()` 공용 헬퍼 신설 |
| S2 | `stop_session.sh` 폴백을 S1 로 교체 + 헤더/`usage()` 갱신 |
| S3 | `update_yaml_resumed.sh` 의 동일 복사본을 S1 로 교체 + 헤더/`usage()` 갱신 |
| S4 | `stop`/`resume`/`create` SKILL.md 및 3개 스크립트 헤더 주석 문서 동기화 |
| S5 | J-1: `_env_int(*names, default=None)` 리팩터(+ **C-2 `continue`**) 및 `main()` 배선 |
| S6 | 회귀 테스트 **8건** 신설 + 기존 J-2 가드 1건 보강 |
| S7 | `IMPROVEMENTS.md` 백로그 등록 및 완료 카운트 갱신 |
| S8 | (분리 커밋) §1.9 `cd … && pwd` 3곳 |

**제외**

| 항목 | 제외 사유 |
|---|---|
| `run_loop.sh:278` 통합 | 호출 12곳 + `claude` 기본값 제거는 행동 변경 → K-1 |
| `reconcile.sh` 해석 경로 | `3aee63cf` §1.2 실측 반증 유효 |
| `agent_of_row` 세그먼트 매칭 | `reconcile.sh` 입양 판정에 영향 → K-4 |
| `max_columns` falsy-zero | 브리프가 min-cols/min-rows 만 지목 → K-2 |
| **`_env_int` 후보 순서 뒤집기** | 문서화된 `MAM_MIN_PANE_COLS` 를 앞으로 옮기는 것은 **우선순위 변경**이라 C-2 (무효값 건너뛰기)와 별개 사안 → **K-5** |
| 레지스트리에 `agent` 필드 쓰기 | 쓰는 코드가 0건이고 요구되지 않음 |

---

## 3. 설계 결정 — 폴백을 **어디에** 넣는가 (Rev.1 유지)

`stop_session.sh` 현재 순서:

```
:88   --session 검사                       → exit 2
:89   YAML 파일 존재 검사                   → exit 1
:97   resolve_herdr_workspace              (load_state_json #1)
:101  AGENT 접미사 추론                     → exit 2  ← 교체 대상
:113  MAPPED_DATA: row 조회                (load_state_json #2)
:124  row 없음                             → exit 1
:152  AGENT 최초 사용
```

**안 A (기각)** — `:113` 블록에 병합. 프로세스 1개 절약, 코드도 가장 깔끔. **기각 사유**: 해석이 row 조회 뒤로 밀려 "미등록 + 이름 해석 실패" 세션의 종료 코드가 **2 → 1** 로 바뀝니다. §1.8 의 두 테스트가 깨지고 헤더 `:28-30` 의 계약도 바뀝니다. 얻는 것은 34 ms 뿐입니다.

**안 B (채택)** — `:101` 자리를 그대로 두고 해석기만 교체.

| 성질 | 결과 |
|---|---|
| 종료 코드 계약 | **불변** (`exit 2`, 동일 메시지) |
| §1.8 기존 테스트 2건 | **수정 불필요** |
| `agy-creator-01` | `EXIT2` → `agy` ✅ |
| `my-project-dev-claude`, `foo-cline`, `worker-1-agy` | `EXIT2` → 정상 해석 ✅ |
| 미래의 `agent` 명시 필드 | 자동 지원 ✅ |
| 비용 | `--agent` 생략 시에만 `load_state_json` 1회 (~34 ms) |

`set -euo pipefail` 주의: 실패 가능한 명령 치환을 대입에 쓰므로 반드시 `|| AGENT=""` 로 감쌉니다(`test_lib_sh_layout_split_in_set_e_subshell` 선례). `stderr` 는 억제하지 않습니다 — 정상 해석 실패는 `sys.exit(1)` 이라 무출력이고, `PYTHONPATH` 파손 같은 진짜 오류의 traceback 은 보여야 합니다. 기존 테스트는 부분 문자열 단언이라 traceback 이 섞여도 무영향입니다.

---

## 4. 구현

### 4.1 S1 — `lib.sh` 공용 헬퍼

`resolve_herdr_session()`(`:989`) 바로 앞에 추가.

```bash
# resolve_agent_type_from_registry <session_name>
#
# 레지스트리(YAML/DB)에 기록된 사실로 에이전트 종류를 해석한다. 우선순위는
# lib_py.agents.registry.agent_of_row 의 계약을 그대로 따른다:
#   ① row['agent'] 명시 필드
#   ② 세션명 접미사 (*-{creator,planner,reviewer}-<agent> 및 *-<agent>)
#   ③ pane.cmd (정확히 일치하거나 .../<agent> 바이너리 경로)
# 성공하면 에이전트명을 stdout 에 출력하고 0 을, 셋 다 실패하면 아무것도
# 출력하지 않고 1 을 반환한다. 오류 메시지는 호출자가 소유한다 — 각 스크립트가
# 문서화한 종료 코드를 그대로 유지하기 위해서다.
#
# NOTE: agent_of_row 의 match_cmd=True 는 "비-입양 조회" 계약이다. reconcile.sh
#       입양 루프는 이 헬퍼를 쓰면 안 된다 (3aee63cf §1.2 실측 반증).
resolve_agent_type_from_registry() {
  local name="$1"
  MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$name" python3 -c "
import os, json, sys
from lib_py.agents.registry import agent_of_row
name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
row = next((s for s in d.get('herdr_sessions', []) if s.get('name') == name), {})
resolved = agent_of_row(row, session_name=name)
if not resolved:
    sys.exit(1)
print(resolved)
"
}
```

**이름을 `resolve_agent_type` 로 하지 않는 이유**: `run_loop.sh:278` 이 동명 함수를 정의하며 `lib.sh` 를 source 합니다. 동명이면 run_loop 의 나중 정의가 조용히 덮어써서 12개 호출 지점이 어느 구현을 쓰는지 읽어서는 알 수 없게 됩니다.

### 4.2 S2 — `stop_session.sh`

`:100-109` 교체:

```bash
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
# 종전과 동일하게 exit 2 (헤더 :27-30 의 종료 코드 계약 유지).
if [ -z "$AGENT" ]; then
  AGENT="$(resolve_agent_type_from_registry "$SESSION_NAME")" || AGENT=""
  [ -n "$AGENT" ] || {
    echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2
    exit 2
  }
fi
```

헤더 `:15-16`:

```
#   --agent <type>          — claude | agy | hermes | cline
#                             (권장: 항상 명시. 미지정 시 레지스트리 기록으로
#                              해석 — agent 필드 → 세션명 접미사 → pane.cmd;
#                              셋 다 실패하면 exit 2)
```

`usage()` `:46-47`:

```
  --agent <type>       — claude | agy | hermes | cline (recommended: always pass it)
                         (falls back to the registry record: agent field ->
                          session-name suffix -> pane.cmd)
```

`usage()` 에 4개 에이전트명이 모두 남아야 합니다 — `test_comp_stop_usage_matches_parser`(`test_tier2_component.py:711-712`)가 단언합니다.

### 4.3 S3 — `update_yaml_resumed.sh`

`:43-52` 를 S2 와 동일한 블록으로 교체(메시지·종료 코드 동일). 헤더 `:7` / `usage()` `:14` 의 `[--agent claude|agy]` → `[--agent claude|agy|hermes|cline]`. `:10` 은 이미 올바른 소싱 형태이므로 손대지 않습니다.

### 4.4 S4 — 문서 동기화

**`multi-agent-mux-stop/SKILL.md`**

Pre-flight(`:38-40`):

```bash
SESSION_NAME=<workspace>-creator-<agent>  # convention
AGENT=claude                              # claude | agy | hermes | cline — always pass it
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
```

워크플로 예제 3개(`:68, :72, :77`)에 `--agent "$AGENT"` 추가:

```bash
# 1. Stop gracefully (default — captures ID, shuts down safely, status=stopped)
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
  --session "$SESSION_NAME" --agent "$AGENT"
```

"Idempotency" 문단(`:81`) 아래에 추가:

```markdown
**`--agent` is the standard.** Pass it on every invocation. If omitted, the script
resolves the agent from the registry record — the row's `agent` field, then the
session-name suffix, then `pane.cmd` — and exits 2 if none of the three resolve.
The fallback exists for recovery, not as the normal calling convention: a session
whose name carries no agent suffix (e.g. `agy-creator-01`) is only resolvable
while its registry row survives.
```

> **Rev.1 에 있던 제약 삭제.** Rev.1 은 이 산문에 `stop_session.sh` 문자열을 쓰지 말라는 제약을 걸어야 했습니다. Rev.2 의 T5 가 펜스 스코프이므로 **그 제약이 필요 없습니다**(§1.10.3~4). 산문을 자유롭게 쓰십시오.

**`multi-agent-mux-resume/SKILL.md:61`** — `AGENT=claude  # or agy or hermes` → `AGENT=claude  # claude | agy | hermes | cline — pass it explicitly`

**`multi-agent-mux-create/SKILL.md`**
- `:146` 동일 수정
- `:171` — 실물 `create_session.sh:86` 과 동일한 `claude, agy, hermes or cline` 문구로. 같은 `case`(`:158-172`)에 `hermes`/`cline` arm 이 없으므로, **스니펫을 축약하고 실물 스크립트를 가리키게 하는 쪽을 권장**합니다. SKILL.md 스니펫이 실물과 갈라지는 것 자체가 이번에 고치는 결함군입니다.

**스크립트 헤더 주석** — `create_session.sh:4`, `resolve_session_id.sh:4` 의 `--agent <claude|agy>` → `<claude|agy|hermes|cline>`

### 4.5 S5 — J-1 (+ C-2)

`layout.py:175-186`:

```python
def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]:
    """First *valid* int among the env vars in *names*, else `default`.

    `default` is an explicit parameter rather than an `or` at the call site so a
    legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 60 default).

    An unparsable value is skipped rather than raised or treated as terminal: a
    typo in an operator's shell must not take the whole layout call down (lib.sh
    would silently fall back to 'right'), and must not shadow a later candidate
    that IS set correctly -- MAM_MIN_COLS is a legacy alias while
    MAM_MIN_PANE_COLS is the name .mam.env.example documents, so aborting on the
    first bad value would discard the documented setting. Empty values already
    fell through; this makes invalid values behave the same way.
    """
    for n in names:
        raw = os.environ.get(n, "").strip()
        if raw:
            try:
                return int(raw)
            except ValueError:
                continue
    return default
```

`main()` `:191-193`:

```python
    parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=60))
    parser.add_argument("--min-rows", type=int, default=_env_int("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", default=20))
    parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS"))
```

`--max-cols` 는 `default=None` 이 의도된 의미(미지정 = 상한 없음)이므로 그대로 둡니다.

**`return default` 가 아니라 `continue` 여야 하는 이유(불변식 확인)**: 모든 후보가 없거나 무효이면 루프가 끝나 `return default` 에 도달합니다. 즉 Rev.1 이 지키려던 "오타 입력은 문서화된 기본값으로 흡수된다"는 성질은 **그대로 유지**되며(§1.11.2 표 4·7행), 달라지는 것은 "첫 후보 무효 + 후속 후보 유효" 한 조건뿐입니다. `min_cols=None` 으로 `compute_2xk_layout` 에 들어가 `TypeError` 가 나는 경로는 두 안 모두에서 발생하지 않습니다.

`*names` 뒤의 키워드 전용 `default` 는 Python 3.9 에서 유효합니다(시스템 인터프리터 3.9.6 실측). `_env_int` 호출자는 `main()` 3곳뿐입니다.

---

## 5. 테스트 계획

신설 **8건**, 기존 가드 보강 **1건**. 예상 collected: **333 → 341**.

> **Rev.1 자체 정정**: Rev.1 은 "신설 8건 → 341" 이라고 적었으나 실제 열거는 7건이었습니다(T1 3 + T3 1 + T4 2 + T5 1). Rev.2 는 C-2 전용 테스트 T1b 를 더해 실제로 8건이 되며, 341 이라는 수치가 비로소 맞아떨어집니다.

### T1 — J-1 env/flag 등가성 (`tests/test_layout.py`, 3건)

```python
_LAYOUT_ENV_VARS = ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", "MAM_MIN_ROWS",
                    "MAM_MIN_PANE_ROWS", "MAM_MAX_COLS", "MAM_MAX_PANE_COLS")

def _run_layout(payload, args=(), env_extra=None):
    env = {**os.environ, "PYTHONPATH": os.path.abspath(".agents/skills")}
    for k in _LAYOUT_ENV_VARS:
        env.pop(k, None)          # 호출자 셸의 오염 차단
    env.update(env_extra or {})
    res = subprocess.run([sys.executable, "-m", "lib_py.layout", "--json", *args],
                         input=json.dumps(payload), capture_output=True, text=True, env=env)
    assert res.returncode == 0, res.stderr
    return json.loads(res.stdout)

# height//2 = 15 < min_rows(20) 로 제약 분기 진입, width//2 = 25 가 min_cols 와 비교됨.
_ZERO_TRAP = {"result": {"panes": [
    {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}}]}}


def test_j1_env_zero_min_cols_matches_flag_zero():
    """J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 60 default."""
    flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0"))
    assert flag["direction"] == "right" and flag["reason"] == "single_pane_height_constrained"
    for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"):
        assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var


def test_j1_env_zero_min_rows_matches_flag_zero():
    flag = _run_layout(_ZERO_TRAP, ("--min-rows", "0"))
    assert flag["direction"] == "down" and flag["reason"] == "single_pane_split_down"
    for var in ("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS"):
        assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var


def test_j1_nonzero_and_malformed_env_behaviour_unchanged():
    """Behaviour neutrality: non-zero env still applies, and a lone typo still
    lands on the documented default instead of crashing on a None comparison."""
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"}) == \
           _run_layout(_ZERO_TRAP, ("--min-cols", "25"))
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "abc"}) == _run_layout(_ZERO_TRAP)
```

### T1b — **[Rev.2 신규]** C-2: 무효값이 뒤 후보를 가리지 않는다 (1건)

```python
def test_j1b_invalid_alias_does_not_shadow_the_documented_var():
    """C-2: MAM_MIN_COLS is a legacy alias checked first; MAM_MIN_PANE_COLS is the
    name .mam.env.example documents. An unparsable value in the alias must be
    skipped, not abort the search and discard the documented setting.

    Empty values already fell through (`if raw:`); this makes invalid values
    behave the same way. When every candidate is unusable, `default` still wins.
    """
    good = _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"})
    assert good["direction"] == "right"
    # 별칭이 깨져 있어도 문서화된 변수가 적용된다
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "25"}) == good
    # 0 도 마찬가지 (J-1 과의 상호작용)
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "0"}) == \
           _run_layout(_ZERO_TRAP, ("--min-cols", "0"))
    # 모든 후보가 무효면 문서화된 기본값으로 흡수 (Rev.1 불변식 보존)
    assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
                                        "MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP)
```

마지막 단언이 중요합니다 — `continue` 로 바꾸면서 Rev.1 이 지키려던 성질이 깨지지 않았음을 같은 테스트 안에서 못 박습니다.

### T2 — J-2 임계값 보강 (기존 `test_headless_max_columns_growth_guard` 확장, 신설 0건)

```python
    # n=5 is the first odd n that can discriminate: n//2 == 2 == max_columns, so an
    # over-correction that also checked the cap on the odd branch would return
    # overflow here. n=3 has n//2 == 1 and cannot reach the check at all.
    d5 = compute_2xk_layout(headless(5), max_columns=2)
    assert d5.direction == "down" and not d5.is_overflow
    assert d5.reason == "headless_odd_down"
```

계획 `5e4ef463` 의 뮤테이션 M6 사양 오류(제가 `n=3` 을 골랐고 그 값으로는 판별 불가)를 닫습니다.

### T3 — `agent_of_row` 단위 보강 (`tests/test_a4_adapter_contract.py`, 1건)

```python
def test_agent_of_row_pane_cmd_binary_path_and_failure():
    # pane.cmd 가 절대 경로 형태여도 해석된다
    assert agent_of_row({'pane': {'cmd': '/usr/local/bin/agy'}}) == 'agy'
    # 세 경로 모두 실패하면 None — 호출자가 오류를 소유한다
    assert agent_of_row({}, session_name='bad-session-name') is None
    # 입양 조회용 match_cmd=False 에서는 pane.cmd 를 보지 않는다
    assert agent_of_row({'name': 'agy-creator-01', 'pane': {'cmd': 'agy'}},
                        match_cmd=False) is None
```

### T4 — `stop_session.sh` 폴백 (`tests/test_tier2_component.py`, 2건)

기존 `test_comp_stop_sqlite_state_update` 의 `run_mutation` 패턴 사용(herdr 부재 → "herdr already dead, just updating YAML" 경로로 rc=0 완주, 실제 세션 미영향).

```python
def test_comp_stop_agent_fallback_reads_pane_cmd(mam_sandbox):
    """B-21: --agent 생략 시 세션명에 에이전트 접미사가 없어도 레지스트리 행의
    pane.cmd 로 해석된다 (라이브 `agy-creator-01` 형태)."""
    mutation = """
d['herdr_sessions'] = [{
    'name': 'agy-creator-01',
    'status': 'running',
    'pane': {'cwd': 'WS_PLACEHOLDER', 'cmd': 'agy'}
}]
""".replace("WS_PLACEHOLDER", str(mam_sandbox))
    run_mutation(mam_sandbox, mutation)
    script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
    res = subprocess.run(["bash", str(script), "--session", "agy-creator-01"],
                         capture_output=True, text=True)
    assert res.returncode == 0, res.stderr
    assert re.search(r"^\s*agent:\s+agy\s*$", res.stdout, re.M), res.stdout


def test_comp_stop_agent_fallback_prefers_explicit_agent_field(mam_sandbox):
    """우선순위 계약: 명시 `agent` 필드가 세션명 접미사와 pane.cmd 를 모두 이긴다."""
    mutation = """
d['herdr_sessions'] = [{
    'name': 'x-creator-claude',
    'status': 'running',
    'agent': 'hermes',
    'pane': {'cwd': 'WS_PLACEHOLDER', 'cmd': 'claude'}
}]
""".replace("WS_PLACEHOLDER", str(mam_sandbox))
    run_mutation(mam_sandbox, mutation)
    script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
    res = subprocess.run(["bash", str(script), "--session", "x-creator-claude"],
                         capture_output=True, text=True)
    assert res.returncode == 0, res.stderr
    assert re.search(r"^\s*agent:\s+hermes\s*$", res.stdout, re.M), res.stdout
```

두 번째가 T4 를 "pane.cmd 를 읽는다" 가 아니라 **"`agent_of_row` 계약을 호출한다"** 로 고정합니다. 첫 번째만 있으면 `pane.cmd` 만 직접 읽는 얕은 구현도 통과합니다.

**미해결 계약(exit 2)** 은 이미 `test_tier1_unit.py:142` 와 `test_tier3_integration.py:398` 이 지킵니다. 두 파일을 **수정하지 않은 채 통과하는 것**이 안 B 의 증거이므로 중복 테스트를 추가하지 않습니다.

### T5 — **[Rev.2 재설계]** 문서 가드 (`tests/test_tier2_component.py`, 1건)

Rev.1 원안은 §1.10.1 의 실측대로 `checked == 2` 로 확정 실패하고, §1.10.2 대로 단일 호출 회귀를 놓칩니다. 챌린저의 **커맨드 단위** 교정을 채택하되, §1.10.3 의 산문 위양성을 막기 위해 **펜스 스코프**를 합성합니다.

```python
# 코드 펜스 안의 stop_session.sh 호출을 '명령 단위'로 잘라낸다.
#  - 펜스 스코프: 산문 속 `stop_session.sh` 언급을 명령으로 오인하지 않는다
#    (Pitfalls / When-NOT-to-use 절은 성격상 스크립트를 산문으로 언급한다).
#  - 명령 단위: 한 펜스에 여러 호출이 들어 있어도 각각을 따로 검증한다
#    (블록 단위로 보면 그중 하나만 --agent 를 가져도 통과해 버린다).
_FENCE_RE = re.compile(r"```(?:bash|sh)\n(.*?)```", re.S)
_STOP_CALL_RE = re.compile(r"(?:bash\s+)?\S*stop_session\.sh[^\n\\]*(?:\\\n[^\n\\]*)*")


def test_comp_docs_stop_examples_pass_agent():
    """B-21 문서 계약: 문서의 모든 stop_session.sh 예제는 --agent 를 넘긴다.
    문서 변경은 뮤테이션 감도가 없으므로 이 가드가 표준의 유일한 집행 장치다."""
    repo = Path(__file__).resolve().parent.parent
    expected = {  # 문서별 최소 예제 수 — 예제를 지워 가드를 무력화하는 것을 막는다
        repo / ".agents/skills/multi-agent-mux-stop/SKILL.md": 3,
        repo / "deploy/INSTALL.md": 2,
    }
    for doc, floor in expected.items():
        seen = 0
        for block in _FENCE_RE.findall(doc.read_text()):
            for m in _STOP_CALL_RE.finditer(block):
                snippet = m.group(0)
                seen += 1
                assert "--agent" in snippet, \
                    f"{doc.name}: stop_session.sh example without --agent:\n{snippet}"
        assert seen >= floor, f"{doc.name}: expected >= {floor} examples, saw {seen}"
```

Rev.1/챌린저안 대비 세 가지가 다릅니다.

| | Rev.1 원안 | 챌린저 수정안 | **Rev.2** |
|---|---|---|---|
| 검증 단위 | 코드 블록 | 명령 | 명령 |
| 스캔 범위 | 펜스 | **문서 전체** | 펜스 |
| 개수 하한 | 전역 `>= 4` (**실패**) | 전역 `>= 5` | **문서별** (3 / 2) |

전역 카운트를 문서별로 쪼갠 이유: 전역이면 SKILL.md 예제 1개가 사라져도 INSTALL.md 가 6개면 통과합니다. 문서별 하한은 실패를 발생 지점에 국소화합니다.

**실측 확인** (§1.10.4): §4.4 적용 후 사본에서 `SKILL.md checked=3 missing=0`, `INSTALL.md checked=2 missing=0`.

### T6 — 회귀 무영향 확인

`bash -n`: `lib.sh`, `stop_session.sh`, `update_yaml_resumed.sh`, `create_session.sh`, `resume_session.sh`, `resolve_session_id.sh`.
`py_compile`: `lib_py/layout.py`. 시스템 파이썬 **3.9.6** 임포트 확인.

### 테스트 파일 사전 조건 2건

1. `test_tier2_component.py:296` 의 `FEATURE 3: Stop Session (4 Test Cases)` 주석 개수 갱신(→ 7). 같은 종류의 드리프트를 새로 만들지 않도록.
2. `test_tier2_component.py` 는 `Path` 는 임포트하지만 **`re` 는 임포트하지 않습니다**(`:1-10`). T4/T5 가 `re` 를 쓰므로 `import re` 추가 필요. `test_layout.py` 는 T1/T1b 가 쓰는 `os/json/subprocess/sys` 를 모두 이미 임포트하고 있어 추가 불필요합니다.

---

## 6. 뮤테이션 매트릭스

격리 사본(`rsync`)에 적용해 지정 테스트가 **FAIL** 하는지 확인.

| # | 뮤테이션 | FAIL 해야 하는 테스트 |
|---|---|---|
| M1 | `stop_session.sh` 폴백을 옛 `case` 블록으로 복원 | `test_comp_stop_agent_fallback_reads_pane_cmd` |
| M2 | 헬퍼에서 `agent_of_row(row, …)` → `agent_of_row({}, session_name=name)` | 위 + `…prefers_explicit_agent_field` |
| M3 | 헬퍼에 `match_cmd=False` 추가 | `…reads_pane_cmd` **만** (두 테스트가 서로 다른 성질을 잡음을 증명) |
| M4 | `_env_int(…, default=60)` → `_env_int(…) or 60` | `test_j1_env_zero_min_cols_matches_flag_zero` |
| M5 | `_env_int` 의 `except ValueError: continue` → `return None` | `test_j1_nonzero_and_malformed_env_behaviour_unchanged` (rc≠0) |
| **M5b** | **[Rev.2]** `except ValueError: continue` → `return default` | `test_j1b_invalid_alias_does_not_shadow_the_documented_var` |
| M6 | 헤드리스 홀수 분기에도 `max_columns` 검사 추가 (과잉 교정) | `test_headless_max_columns_growth_guard` (신설 `d5` 단언) |
| M7 | `SKILL.md` 예제 **한 곳**에서 `--agent` 삭제 | `test_comp_docs_stop_examples_pass_agent` |
| **M7b** | **[Rev.2]** `INSTALL.md` 의 **두 호출 중 하나**에서만 `--agent` 삭제 | 동일 (§1.10.2 에서 이미 선실측: Rev.1 설계는 미검출, Rev.2 설계는 `missing=1` 검출) |
| **M7c** | **[Rev.2]** `SKILL.md` 워크플로 예제 1개를 통째로 삭제 | 동일 (`seen >= 3` 하한) |
| M8 | `update_yaml_resumed.sh` 폴백을 옛 `case` 블록으로 복원 | — **가드 없음** |

**M8 을 정직하게 남깁니다.** `update_yaml_resumed.sh` 의 폴백은 유일한 생산 호출자인 `resume_session.sh:66, :129` 가 항상 `--agent "$AGENT"` 를 명시하므로 **그 경로에서 도달 불가**합니다. 직접 호출 시에만 살아납니다. 도달 불가 경로를 위해 별도 픽스처를 세우는 대신 S3 는 "중복 제거"로 정당화하고 가드 없음을 명시합니다. 리뷰어가 이 판단에 이의가 있으면 T4 와 동형의 테스트 추가가 옳은 처방입니다.

**M5 와 M5b 가 서로 다른 테스트를 깨는 것**이 C-2 반영의 검증 조건입니다. M5(=`None` 복귀)는 크래시 경로를, M5b(=Rev.1 안으로 복귀)는 별칭 섀도잉을 각각 잡습니다. 둘 다 잡히지 않으면 T1b 가 의미 없는 테스트라는 뜻입니다.

---

## 7. 커밋 분할

| # | 커밋 | 내용 |
|---|---|---|
| 1 | `feat(lib,stop,resume): resolve --agent from the registry via agent_of_row (B-21)` | S1 + S2 + S3 + T3 + T4 |
| 2 | `docs(skills): standardize explicit --agent across stop/resume/create guides (B-21)` | S4 + T5 |
| 3 | `fix(layout): make _env_int take an explicit default and skip invalid values (J-1)` | S5 + T1 + T1b |
| 4 | `test(layout): cover the headless growth-guard threshold at n=5 (J-2)` | T2 |
| 5 | `docs(improvements): register J-1/J-2/B-21 and refresh the completed count` | S7 |
| 6 | `fix(scripts): repair the dead lib.sh sourcing path in stop/create/resume` | S8 (§1.9) |

커밋 1~4 는 각각 독립 revert 가능합니다. 커밋 6 은 §1.9 가 브리프 범위 밖의 별개 사안이므로 분리합니다 — 리뷰어가 범위 이탈로 판단하면 이 커밋만 드롭하면 됩니다.

커밋 3 의 제목이 Rev.1 에서 바뀌었습니다(`… and skip invalid values` 추가). C-2 가 J-1 과 다른 성질의 변경이므로 제목이 그 사실을 담아야 합니다.

---

## 8. `IMPROVEMENTS.md` 갱신 (S7)

J-1 / J-2 는 현재 `IMPROVEMENTS.md` 에 **등록돼 있지 않습니다**(`55d1a1d9` 리뷰 보고서에만 존재).

**ID 충돌 경고**: `IMPROVEMENTS.md:301` 의 `C-1`("Kanban 문서 29회 언급 vs 실제 구현 0건")과 `:37` 이 참조하는 `C-1`(레이아웃 헤드리스 `max_columns`)은 **서로 다른 두 과제가 같은 ID** 를 씁니다. 신규는 `J-1`/`J-2`/`B-21` 을 씁니다. 기존 충돌은 K-3.

갱신 항목:

1. `:3` 최종 갱신일
2. `:6` 총 추적 미해결 과제 카운트
3. `:7` 완료 과제 **29 → 30** 및 목록에 `B-21` 추가
4. `:37` B-20 후속 정리 줄에 J-1/J-2 해소 한 줄
5. §2 에 `B-21` 절 신설 — 현상(라이브 `agy-creator-01` 이 `--agent` 없이 `exit 2`), 원인(해석기 4중화), 조치, 회귀 가드
6. §6.2 로드맵 표에 완료 행
7. §6.3 파일 소유권 슬롯 표 갱신

---

## 9. 후속 백로그 (이번 범위 밖, 등록만)

| ID | 내용 | 근거 |
|---|---|---|
| **K-1** | `run_loop.sh:278` `resolve_agent_type` 통합 | §1.1 — 해석 실패를 `claude` 로 흡수. cline 세션에 claude 종료키를 보내는 오분류가 구조적으로 가능. 호출 12곳이라 별도 계획 필요 |
| **K-2** | `compute_2xk_layout` 의 `if max_columns and …` falsy-zero | `--max-cols 0`("열 0개")이 "상한 없음"으로 흡수됨. J-1 과 동일 부류 |
| **K-3** | `IMPROVEMENTS.md` 의 `C-1` ID 충돌 정리 | §8 |
| **K-4** | `agent_of_row` 에 하이픈 세그먼트 매칭 추가 여부 | `orc-hermes-main` 류 미해결. `reconcile.sh` 입양 판정 영향 → 실측 선행 |
| **K-5** | **[Rev.2 신규]** `_env_int` 후보 **순서** 재검토 | §1.11.1 — 문서화되지 않은 레거시 `MAM_MIN_COLS` 가 `.mam.env.example` 이 문서화한 `MAM_MIN_PANE_COLS` 보다 **우선**합니다. C-2(무효값 건너뛰기)는 이 순서 문제를 완화할 뿐 해소하지 않습니다. 둘 다 유효한 값이면 여전히 레거시가 이깁니다. 순서 변경은 행동 변경이므로 별도 항목 |

---

## 10. 검증 절차 (Creator 실행)

```bash
# 1) 구문
for f in .agents/skills/lib.sh \
         .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
         .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \
         .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh \
         .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
         .agents/skills/multi-agent-mux-create/scripts/create_session.sh; do
  bash -n "$f" || echo "FAIL $f"
done
python3 -m py_compile .agents/skills/lib_py/layout.py

# 2) J-1 직접 확인 (0 이 살아남는가)
P='{"result":{"panes":[{"pane_id":"p1","rect":{"x":0,"y":0,"width":50,"height":30}}]}}'
printf '%s' "$P" | PYTHONPATH=.agents/skills MAM_MIN_PANE_COLS=0 python3 -m lib_py.layout --json
printf '%s' "$P" | PYTHONPATH=.agents/skills python3 -m lib_py.layout --json --min-cols 0
#   → 두 출력이 완전히 동일하고 direction=right

# 3) C-2 직접 확인 (무효 별칭이 문서화된 변수를 가리지 않는가)
printf '%s' "$P" | PYTHONPATH=.agents/skills \
  env MAM_MIN_COLS=foo MAM_MIN_PANE_COLS=25 python3 -m lib_py.layout --json
#   → direction=right (수정 전에는 overflow)

# 4) 전체 스위트 (베이스라인 333 → 기대 341)
.venv/bin/python -m pytest tests/ -q

# 5) 배포 신선도 (기존 31건 유지)
.venv/bin/python -m pytest tests/test_deploy_freshness.py -q

# 6) 뮤테이션 M1~M7c (격리 사본에서)
```

**금지 사항**: `tests/test_tier1_unit.py:142` 와 `tests/test_tier3_integration.py:398` 은 **수정하지 않습니다**. 두 건이 무수정으로 PASS 하는 것이 안 B 의 종료 코드 계약 보존을 입증하는 증거입니다. 고쳐야 통과한다면 구현이 안 A 로 흘러간 것이므로 되돌려야 합니다.

**라이브 세션 보호**: T4 는 `mam_sandbox` 안에서만 동작하며 실제 `.mam/agent-sessions.yaml` 을 건드리지 않습니다. 개발 중 `stop_session.sh` 를 실 워크스페이스에서 수동 실행하지 마십시오 — `canary-projects-multi-agent-mux-creator-claude` 가 이 세션입니다.

---

## 11. 규모 추정

| 파일 | 변경 |
|---|---|
| `.agents/skills/lib.sh` | +22 (헬퍼 1개) |
| `stop_session.sh` | +8 / −10, 헤더·usage +6 |
| `update_yaml_resumed.sh` | +8 / −9, 헤더·usage +2 |
| `lib_py/layout.py` | +10 / −6 (docstring 확장 포함) |
| `multi-agent-mux-stop/SKILL.md` | +12 |
| `multi-agent-mux-resume/SKILL.md` | +1 / −1 |
| `multi-agent-mux-create/SKILL.md` | +2 / −2 |
| `create_session.sh` / `resolve_session_id.sh` | 헤더 각 +1 / −1 |
| `tests/test_layout.py` | +62 (T1 45 + T1b 17) |
| `tests/test_a4_adapter_contract.py` | +9 |
| `tests/test_tier2_component.py` | +58 |
| `IMPROVEMENTS.md` | +20 |
| (커밋 6) 3개 스크립트 소싱 줄 | +3 / −3 |

총 **약 +215 / −35 줄**, 파일 12개. 규모 **소~중**.

---

## 12. 챌린저에게

C-1 은 계획대로 짜면 확정 실패하는 결함이었고, 두 갈래 모두 정확했습니다. 특히 갈래 ②(부분 문자열 위양성)는 **테스트가 통과하기 때문에 아무도 눈치채지 못하는** 종류라 더 값어치가 있습니다. §1.10.2 에서 뮤테이션으로 재현했습니다.

수정안을 그대로 채택하지 않은 부분은 한 곳입니다 — 문서 전체 스캔이 산문 속 파일명 언급을 명령으로 오인합니다(§1.10.3, 위양성 2건 실측). 이 계획 §4.4 자체가 SKILL.md 에 산문을 추가하므로 임박한 문제였습니다. 커맨드 단위라는 **핵심 교정은 그대로 채택**하고 펜스 스코프를 얹었습니다.

C-2 는 제기하신 근거(다중 fallback 취지)보다 강한 근거가 실측에서 나왔습니다. `MAM_MIN_COLS` 는 `layout.py` 밖 어디에도 없는 레거시 별칭이고, 가려지는 `MAM_MIN_PANE_COLS` 는 `.mam.env.example` 이 문서화한 **유일한** 이름입니다. 게다가 현행 코드는 `""` 는 건너뛰고 `"foo"` 는 탈출하는 비대칭을 갖고 있습니다. 수용하고 전용 테스트 T1b 를 신설했습니다.
