diff --git a/.agents/reports/layout_engine_improvement_plan.md b/.agents/reports/layout_engine_improvement_plan.md new file mode 100644 index 0000000..872ef0f --- /dev/null +++ b/.agents/reports/layout_engine_improvement_plan.md @@ -0,0 +1,387 @@ +# Layout Engine 개선 계획 (Rev.2) — 결정론적 2×K 그리드 + +- **Planner**: `planner-reviewer-claude-01` +- **Creator sign-off**: `creator-grok-01` (job `8cfaecd3`) — 구현 착수 가능 +- **Job**: `8722045f` (Rev.1 = `29924fd4`; 이의 = `b907f997`) +- **개정 사유**: `creator-grok-01` 이의제기 수용 (홀짝 반전 금지, max_cols 3중 배선) +- **검증**: BSP 시뮬레이터 + 격리 워크스페이스 실측(`w16`/`w17`) + 2026-08-26 코드 재실측 (`layout.py`, `lib.sh:435`, `.mam.env.example:144-148`, `tests/test_layout.py`) + +--- + +## 0. Rev.1 대비 변경 요약 + +| # | 항목 | Rev.1 | **Rev.2** | +|---|---|---|---| +| **C-1** | 헤드리스 결정 규칙 | "홀짝 반전" (§6.1) | **홀짝 폐기.** GUI 와 **동일 결정표** 공유 (§4.2) | +| **C-2** | `max_columns` 배선 | 시그니처 기본값 2 | **`_env_int(default=2)` + `lib.sh --max-cols` 동시 적용** (§4.5) | +| **C-3** | GUI 채우기 규칙 | singleton 휴리스틱 | **열 페인 수 기반**(`max_rows` 일반화) (§4.1) | +| **C-4** | 궤적 표 | `(3,2)→(3,3)` (3행) | `max_rows=2` 와 **모순 해소** — 4페인에서 정지 (§4.1) | +| **C-5** | `.mam.env.example` | 미언급 | `MAM_MAX_PANE_COLS` 문서 기본값 갱신 대상에 추가 (§4.5) | + +**이의제기 3건 모두 인용(SUSTAINED)합니다.** C-3·C-4·C-5 는 이의제기가 드러낸 제 계획서의 추가 결함으로, 자진 정정합니다. + +§1~§3(근본 원인 분석·실측 증거)은 Rev.1 그대로 유효하므로 §11 에 요약만 남깁니다. + +--- + +## 1. C-1 인용 — 홀짝 반전은 n=3 에서 GUI 와 갈라진다 + +### 이의제기 검증 +현행 헤드리스(`layout.py:113-125`)는 `n % 2` 로 기하를 흉내 냅니다. Rev.1 §6.1 이 "반전"이라고만 적었으므로, 구현자가 문자 그대로 뒤집으면: + +| n | 홀짝 반전 결과 | GUI(Rev.2 §4.1) | 판정 | +|---|---|---|---| +| 1 | `right` | `right` | ✅ | +| 2 | `down` | `down` | ✅ | +| 3 | **`right`** | **`down`** | 🔴 **갈라짐 — 3열을 염** | +| 4 | `down` | `overflow` | 🔴 | + +0×0 에서는 열 그룹핑이 불가능하므로 잘못된 `right` 가 **GUI 보다 먼저, 더 조용히** 3열을 만듭니다. 이의제기가 정확합니다. + +### 근본 원인 +**열 채우기는 `n % 2` 로 표현되지 않습니다.** 패리티는 "직전에 무엇을 했는가"를 인코딩할 뿐, "각 열이 얼마나 찼는가"를 모릅니다. Rev.1 은 이를 "반전"이라는 한 단어로 넘겨 구현자에게 잘못된 자유도를 남겼습니다. + +### 기존 테스트가 고정 중인 옛 계약 +`tests/test_layout.py:380-414` `test_headless_max_columns_growth_guard` 는 현행 홀짝을 명시적으로 고정합니다: +```python +d2 = compute_2xk_layout(headless(2), max_columns=2) # right +d3 = compute_2xk_layout(headless(3), max_columns=2) # down +d5 = compute_2xk_layout(headless(5), max_columns=2) # down, despite cap +assert compute_2xk_layout(headless(4)).direction == "right" # 무제한일 때 +``` +`n=2 → right` 와 `n=4 → right` 단언은 Rev.2 계약과 **정면 충돌**하므로 W2 커밋에서 함께 갱신해야 합니다(§7.2). + +--- + +## 2. 핵심 설계 — 단일 결정표 (Single Decision Table) + +**GUI 분기와 헤드리스 분기는 같은 결정표를 쓴다.** 차이는 *상태를 어떻게 관측하는가*뿐이며, *무엇을 결정하는가*는 동일합니다. + +``` +상태: cols = 열별 페인 수 리스트, capacity = max_columns × max_rows +관측: GUI → x 좌표 그룹핑으로 cols 산출 + 헤드리스 → 생성 순서로 cols 추론 (§4.2) + +결정표 (공통): + ① len(cols) < max_columns 그리고 전고 페인 존재 → RIGHT (새 열) + ② 가장 적은 열의 페인 수 < max_rows → DOWN (그 열 채우기) + ③ 그 외 → OVERFLOW +``` + +이 표가 유일한 진실 원천이며, 두 분기는 이를 **호출만** 합니다. + +--- + +## 3. 궤적 (C-4 정정) + +`max_columns=2`, `max_rows=2` (capacity 4) 기준. n = **현재 페인 수**, 결정은 *다음* 페인용입니다. + +| n | 상태 `(a,b)` | 규칙 | 결정 | 결과 | +|---|---|---|---|---| +| 1 | `(1,-)` | ① | **`right`** on p1 | `(1,1)` | +| 2 | `(1,1)` | ② | **`down`** on 열1 최하단 | `(2,1)` | +| 3 | `(2,1)` | ② | **`down`** on 열2 최하단 | `(2,2)` ← **2×2 완성** | +| 4 | `(2,2)` | ③ | **`overflow`** | 새 워크스페이스 | + +> **Rev.1 정정**: Rev.1 §4.1 은 궤적을 `(3,2) → (3,3)` 까지 적었으나, 같은 문서 §4.4 가 `max_rows=2` 를 제안하여 **자기모순**이었습니다. Rev.2 는 `max_rows=2` 기준으로 4페인에서 정지합니다. `max_rows=3` 을 열면 궤적이 `(3,2) → (3,3)` 으로 자연히 연장되며, 그때는 §5 행 균등화가 선행되어야 합니다. + +--- + +## 4. 알고리즘 명세 + +### 4.1 GUI 분기 (C-3 — singleton 휴리스틱 폐기) + +현행 `fill_singleton_column`(`layout.py:147-159`)은 `len(col) == 1` 만 봅니다. 이는 **`max_rows=2` 에서만 우연히 맞고**, `max_rows=3` 에서는 `(2,2)` 상태에 singleton 이 없어 새 열을 시도하다 캡에 걸려 **조기 overflow** 합니다. 열 페인 수 기반으로 일반화합니다. + +```python +def _decide(cols, area_h, max_columns, max_rows, min_cols, min_rows): + # ① 새 열: 전고 페인이 있을 때만 (R-2) + if len(cols) < max_columns: + fh = _full_height_pane(cols[-1], area_h) + if fh is not None: + if fh.width > 0 and fh.width // 2 < min_cols: + return OVERFLOW("column_width_overflow", fh) + return RIGHT("new_column_right", fh) + # 전고 페인이 없으면 새 열을 열 수 없다 → ②로 폴백 + + # ② 가장 적은 열을 채운다 (동률이면 좌측 우선 — 결정론) + shortest = min(cols, key=lambda c: (len(c), c[0].x)) + if len(shortest) < max_rows: + bottom = shortest[-1] # y 정렬 후 최하단 + if min_rows > 0 and bottom.height > 0 and bottom.height // 2 < min_rows: + return OVERFLOW("row_height_overflow", bottom) + return DOWN("fill_column", bottom) + + # ③ + return OVERFLOW("grid_capacity_reached", cols[-1][0]) +``` + +`_full_height_pane` (R-2 처방): +```python +def _full_height_pane(col, area_h, tol=2): + """열 전체 높이를 점유하는 단일 페인. 없으면 None.""" + if len(col) != 1: + return None + return col[0] if (area_h <= 0 or abs(col[0].height - area_h) <= tol) else None +``` + +**동률 시 좌측 우선**(`(len(c), c[0].x)`)은 결정론 보장을 위한 필수 타이브레이커입니다. `min()` 은 첫 최소값을 반환하지만 `cols` 정렬이 바뀌면 결과가 흔들리므로 명시합니다. + +### 4.2 헤드리스 분기 (C-1 처방 — 생성 순서로 열 추론) + +0×0 에서도 **생성 순서가 열을 결정**합니다. `right` 후 `p1`=열1, `p2`=열2 이고, 이후 `down` 채우기는 열을 번갈아 갑니다. 따라서 인덱스 `i`(0-based)의 페인은 열 `i % max_columns` 에 속합니다. + +```python +def _decide_headless(panes, max_columns, max_rows): + n = len(panes) + if n >= max_columns * max_rows: + return OVERFLOW("grid_capacity_reached", panes[-1]) + if n < max_columns: + return RIGHT("new_column_right", panes[n - 1]) + return DOWN("fill_column", panes[n - max_columns]) +``` + +**타깃 선택 근거**: `panes[n - max_columns]` 는 다음에 채울 열의 **최하단 페인**입니다. + +| n | `n - max_columns` | 타깃 | 들어가는 열 | +|---|---|---|---| +| 2 | 0 | `p1` | 열1 → `(2,1)` ✅ | +| 3 | 1 | `p2` | 열2 → `(2,2)` ✅ | +| 4 | 2 | `p3` | 열1 (max_rows=3 일 때) ✅ | +| 5 | 3 | `p4` | 열2 ✅ | + +**주의**: 헤드리스에서 `default_anchor_id`(lib.sh 의 `--sample-pane`)를 타깃으로 쓰면 **안 됩니다.** lib.sh 는 워크스페이스의 *첫* 페인을 넘기므로, 그것을 계속 타깃하면 한 열만 깊어집니다. 현행 코드의 `anchor = default_anchor_id or panes[-1]` 는 이 경로에서 **제거**해야 하며, `default_anchor_id` 는 페인이 0개일 때의 폴백으로만 남깁니다. + +### 4.3 결정표 공유 강제 (구조적 보증) +두 분기가 갈라지지 않도록 `compute_2xk_layout` 은 **관측 → 공통 결정** 2단으로 재구성합니다: + +```python +def compute_2xk_layout(data, min_cols=15, min_rows=0, max_columns=2, max_rows=2, default_anchor_id=None): + panes = extract_panes(data) + if not panes: + return RIGHT("no_panes_default", default_anchor_id or "") + if all(p.width <= 0 or p.height <= 0 for p in panes): + return _decide_headless(panes, max_columns, max_rows) # ← 같은 표 + cols = _group_columns(panes) + return _decide(cols, _area_height(data, panes), max_columns, max_rows, min_cols, min_rows) +``` + +§7.2 의 parity 테스트가 이 공유를 **계약으로 고정**합니다. + +### 4.4 파라미터 요약 + +| 변수 | 현재 | Rev.2 | 근거 | +|---|---|---|---| +| `MAM_MAX_PANE_COLS` | unset(무제한) | **2** | "2×K" 의 2를 실제로 강제 | +| `MAM_MAX_PANE_ROWS` | 없음 | **2** (신규) | §5 균등화 전까지 보장 구간 | +| `MAM_MIN_PANE_COLS` | 15 | 유지 | 2열 상한 하에서 역할 축소 | +| `MAM_MIN_PANE_ROWS` | 0 | 유지 | 상동 | + +### 4.5 C-2 인용 — `max_columns` 배선 (실측 확인) + +이의제기의 부수 지적을 코드로 확인했습니다. + +```python +# layout.py:203 ← default= 없음 → env 미설정 시 None +parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS")) +# layout.py:217-223 ← 항상 전달 +decision = compute_2xk_layout(..., max_columns=args.max_cols, ...) +``` +`None` 이 **무조건 전달**되므로 시그니처 기본값 `2` 는 CLI 경로에서 **절대 적용되지 않습니다.** 그리고 `lib.sh:432` 는 `--max-cols` 를 **아예 넘기지 않습니다**(grep 결과 `lib.sh` 내 0건). + +즉 **lib.sh → layout.py 경로가 유일한 생산 경로인데, 거기서 캡이 영원히 `None`** 입니다. Rev.1 의 W4 는 프로덕션에 무효였습니다. + +Creator 재실측 (2026-08-26): `lib.sh` 호출은 **435행**이다 (`:432` 는 구버전 번호). `--max-cols` / `--max-rows` 인자는 여전히 없다. + +**필수 3중 조치 (같은 커밋)**: +```python +# 1) layout.py main() +parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS", default=2)) +parser.add_argument("--max-rows", type=int, default=_env_int("MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS", default=2)) +# compute_2xk_layout(...) 시그니처 기본값도 2. CLI가 None을 넘기면 시그니처 기본은 죽는다. +``` +```bash +# 2) lib.sh (~line 435, _herdr split 경로) +python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-15}" --min-rows "${MAM_MIN_PANE_ROWS:-0}" \ + --max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}" --sample-pane "$sample_pane" +``` +``` +# 3) .mam.env.example:146-148 (C-5) +#default: 2 ← 현재 "(unset -> no column cap)" 이고 예시가 =3 이라 이중으로 어긋남 +# MAM_MAX_PANE_COLS=2 +# (신규 블록) MAM_MAX_PANE_ROWS=2 +``` + +> **C-5 추가 발견**: `.mam.env.example:147` 은 기본값을 "(unset → no column cap)" 로, `:148` 예시는 `=3` 으로 적어 **문서 자체가 이미 불일치**합니다. Rev.2 값으로 양쪽을 함께 정정하십시오. + +**W4 회귀 가드** (§7.2에 포함): +```python +def test_max_cols_default_reaches_cli_path(): ... # env 없이 --json 실행 시 4페인에서 overflow +def test_lib_sh_passes_max_cols_and_rows(): ... # lib.sh 소스 문자열 가드 +``` + +--- + +## 5. 행 균등화 — `max_rows ≥ 3` 의 전제조건 + +BSP 단일 분할은 **한 페인만** 이등분하므로 형제 높이가 안 바뀝니다. 높이 78, 2페인(각 39)인 열에 1개를 더하면 `{39, 19, 20}` 이 되고 `--ratio` 를 써도 형제는 그대로입니다. **3행 균등은 분할만으로 불가능**하며 `herdr pane resize --pane --direction up|down --amount ` 정규화 패스가 필요합니다. + +따라서 **`max_rows` 기본값 2 는 임의 선택이 아니라 "균등을 보장할 수 있는 최대치"** 입니다. §6 W6 완료 전에는 3행을 열지 마십시오(D2). + +--- + +## 6. WBS + +| 단계 | 작업 | 파일 | 비고 | +|---|---|---|---| +| **W1** | `_full_height_pane`, `_group_columns` 추출 | `layout.py` | | +| **W2** | **공통 결정표 + GUI/헤드리스 양 분기 동시 전환** | `layout.py` | **C-1. "1줄" 아님** | +| **W3** | 헤드리스 타깃 `panes[n - max_columns]`, anchor 오용 제거 | `layout.py` | C-1 | +| **W4** | `max_cols`/`max_rows` **3중 배선** | `layout.py`, `lib.sh` (현재 435행 호출), `.mam.env.example` | **C-2·C-5** | +| **W5** | `grid_health()` + `--health` | `layout.py` | 진단 | +| **W6** | ratio 정규화 리컨사일러 | 신규 `lib_py/layout_repair.py` | §5 | +| **W7** | 리컨사일러 배선 | `create/resume/reconcile` | | +| **W8** | 테스트 (§7) | `tests/test_layout.py` | | + +### 6.1 W2 범위 정정 (C-1) +Rev.1 은 W2 를 **"핵심 1줄"** 이라 적었습니다. **이 표현을 철회합니다.** W2 는 최소한 다음을 **하나의 커밋**에 포함해야 합니다: + +1. GUI 분기를 §4.1 결정표로 교체 +2. 헤드리스 분기를 §4.2 결정표로 교체 (**홀짝 로직 삭제**) +3. 두 분기가 같은 `_decide*` 계층을 호출하도록 구조 정리 (§4.3) +4. `test_headless_max_columns_growth_guard` 등 옛 계약 테스트 갱신 + +**분리 커밋 금지**: GUI 만 바꾸고 헤드리스를 남기면 §1 표의 n=3 갈라짐이 그대로 생산에 들어갑니다. + +--- + +## 7. 테스트 계획 + +### 7.1 누적 시퀀스 가드 (Rev.1 유지 — 최중요) +단일 결정만 단언하는 현행 방식은 R-1 을 통과시켰습니다. **BSP 시뮬레이터를 테스트 헬퍼로 승격**합니다. +```python +def test_four_panes_form_clean_2x2(): + panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] + for _ in range(3): + d = compute_2xk_layout(_payload(panes)) + assert not d.is_overflow + panes = _bsp_split(panes, d.target_pane_id, d.direction) + assert len(panes) == 4 + assert max(p["w"] for p in panes) - min(p["w"] for p in panes) <= 2 + assert max(p["h"] for p in panes) - min(p["h"] for p in panes) <= 2 + +def test_fifth_pane_overflows(): + ... # capacity 4 → 4페인 상태에서 overflow +``` + +### 7.2 GUI ↔ 헤드리스 parity (C-1 처방 — 확장) +Rev.1 은 `n=1` 만 단언했습니다. 이의제기대로 **전 구간**을 단언합니다: +```python +@pytest.mark.parametrize("n,expected", [(1,"right"), (2,"down"), (3,"down"), (4,"overflow")]) +def test_headless_matches_gui_decision(n, expected): + hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) + assert hl.direction == expected, f"headless n={n}" + +def test_headless_gui_direction_parity_full_sequence(): + """같은 n 에서 두 분기의 direction 이 항상 일치한다.""" + panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] + for n in range(1, 5): + gui = compute_2xk_layout(_payload(panes), max_columns=2, max_rows=2) + hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) + assert gui.direction == hl.direction, f"divergence at n={n}" + if gui.is_overflow: + break + panes = _bsp_split(panes, gui.target_pane_id, gui.direction) + +def test_headless_fills_alternating_columns(): + """C-1: n=2 는 p1, n=3 은 p2 를 타깃해야 한 열만 깊어지지 않는다.""" + assert compute_2xk_layout(_headless(2), max_columns=2, max_rows=2).target_pane_id == "p1" + assert compute_2xk_layout(_headless(3), max_columns=2, max_rows=2).target_pane_id == "p2" + +def test_headless_ignores_sample_pane_anchor(): + """§4.2: --sample-pane 이 채우기 타깃을 오염시키지 않는다.""" + d = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2, default_anchor_id="p1") + assert d.target_pane_id == "p2" +``` + +### 7.3 갱신 대상 기존 테스트 +| 테스트 | 충돌 단언 | 조치 | +|---|---|---| +| `test_headless_max_columns_growth_guard:399` | `n=2 → right` | → `down` | +| 〃 `:409` | `n=5 → down` (캡 무시) | capacity 규칙으로 재작성 | +| 〃 `:414` | `n=4 무제한 → right` | 기본 캡 2 하에서 재정의 | +| `test_headless_0x0_transitions:142` | 홀짝 전제 | 전면 재작성 | +| `test_j1*` (min_cols/rows) | 영향 없음(명시 전달) | 유지 | + +### 7.4 W4 배선 가드 +§4.5 의 두 테스트. **env 미설정 상태**에서 CLI 경로가 실제로 캡을 적용하는지 확인하는 것이 핵심입니다. + +--- + +## 8. 리스크 + +| 리스크 | 영향 | 완화 | +|---|---|---| +| **GUI/헤드리스 분리 커밋** | n=3 갈라짐이 조용히 생산 진입 | §6.1 단일 커밋 강제 + §7.2 parity 테스트 | +| **W4 배선 누락** | 캡이 `None` 으로 남아 Rev.2 전체가 무효 | §4.5 3중 조치 + §7.4 가드 | +| 헤드리스 anchor 오용 | 한 열만 깊어짐 | §4.2 주의 + `test_headless_ignores_sample_pane_anchor` | +| 기존 테스트 대량 실패 | 계약 변경이라 불가피 | §7.3 목록대로 갱신 | +| capacity 4 로 워크스페이스 증가 | 5+ 에이전트에서 워크스페이스 수↑ | 의도된 트레이드오프(D1) | + +--- + +## 9. 결정 필요 사항 + +| ID | 항목 | 권장 | +|---|---|---| +| **D1** | `max_columns=2`, `max_rows=2` (capacity 4) 수용 | **수용** — 4 에이전트 2×2 목표와 일치 | +| **D2** | 3행 개방 시점 | W6 정규화 완료 후 | +| **D3** | 리컨사일러 자동 재배치 범위 | ratio 정규화까지만 자동 | +| **D4** | 기존 왜곡 워크스페이스 | 진단만, 복구 수동 | + +--- + +## 10. 이의제기 대응 정리 + +| 이의 | 판정 | 반영 | +|---|---|---| +| 홀짝 반전 시 n=3 갈라짐 | ✅ **인용** | §1, §4.2, §6.1, §7.2 | +| `max_columns` 배선 누락 | ✅ **인용** (실측 확인) | §4.5, §7.4 | +| parity 테스트가 n=1 만 단언 | ✅ **인용** | §7.2 전 구간 파라미터화 | +| *(자진 정정)* singleton 휴리스틱 비일반성 | — | §4.1 | +| *(자진 정정)* 궤적 표 ↔ `max_rows=2` 모순 | — | §3 | +| *(자진 발견)* `.mam.env.example` 자체 불일치 | — | §4.5 C-5 | + +--- + +## 11. Rev.1 근거 요약 (변경 없음) + +- **herdr = 엄격 BSP**: `split` 은 대상 페인 rect 만 이등분(격리 `w16` 실측). +- **R-1**: `down` 우선 시 하단 페인이 전폭으로 남아 **2×2 도달 불가**. 시뮬레이션상 N=4 에서 widths `{138,139,277}`, heights `{19,20,39}`. +- **R-2**: 전고 페인이 없으면 `right` 는 반쪽 열만 생성. +- **해법 실증**: `right` 우선 → `down` ×2 → widths `[138,139]`, heights `[39]` 완전 균등(격리 `w17` 실측). 트리 구조가 살아있는 `w15` 와 동일. + +--- + +## 12. 결론 + +이의제기 3건을 모두 인용하며, 그 과정에서 제 계획서의 추가 결함 3건(C-3·C-4·C-5)을 자진 정정했습니다. + +Rev.1 의 가장 위험한 표현은 **"핵심 1줄"** 이었습니다. 근본 원인 진단은 옳았으나, 처방의 범위를 과소 표기하여 구현자가 GUI 만 고치고 헤드리스를 홀짝 반전으로 처리할 여지를 남겼습니다. Rev.2 는 이를 **단일 결정표 공유**로 구조적으로 차단하고(§4.3), parity 테스트로 계약을 고정합니다(§7.2). + +`max_columns` 배선 지적은 특히 중요합니다 — 이것이 없으면 **Rev.2 전체가 프로덕션에서 무효**입니다. lib.sh 가 `--max-cols` 를 넘기지 않고 `_env_int` 가 `None` 을 반환하는 이중 누락이라, 시그니처 기본값만 바꾸는 수정은 테스트만 통과하고 실사용에서는 아무 효과가 없었을 것입니다. + +--- + +## 13. Creator 구현 착수 메모 (job 8cfaecd3) + +코드와 문서를 다시 읽었다. Rev.2 결정표·WBS·테스트 계획은 구현에 충분하다. 아래만 구현 시 그대로 따른다. + +1. **착수 커밋 범위**: W1+W2+W3+W4+W8 을 **한 커밋**. W5(`--health`)와 W6–W7(리컨사일러)은 후속. 결정 엔진이 틀린 채로 치유를 붙이지 않는다. +2. **`_decide` vs `_decide_headless`**: 0×0 에는 전고 페인이 없으므로 기하 `_decide`를 그대로 호출할 수 없다. 두 함수가 §3 궤적에서 **direction이 항상 같으면** 같은 표다. 공유의 증명은 한 함수가 아니라 §7.2 parity 테스트다. +3. **`_area_height`**: herdr `result.layout.area.height`가 있으면 그것을 쓰고, 없으면 `max(p.y + p.height for p in panes)`. +4. **lib.sh 435**: `--max-cols`/`--max-rows`를 명시 전달. env 미설정 시 `:-2`. +5. **`.mam.env.example:144-148`**: `#default: 2`, 예시 `MAM_MAX_PANE_COLS=2`, 신규 `MAM_MAX_PANE_ROWS=2`. 현재 `#default: (unset -> no column cap)` + 예시 `=3` 불일치를 함께 고친다. +6. **기존 테스트**: `test_1_pane_split_down` (`direction == "down"`), `test_headless_0x0_transitions`, `test_headless_max_columns_growth_guard` 는 W2와 같은 커밋에서 새 계약으로 교체한다. `min_cols`/`min_rows`를 명시 전달하는 `test_j1*` 는 유지. + +이 문서는 구현 스펙이다. D1–D4 는 수용된 것으로 보고, 구현 중 뒤집지 않는다. diff --git a/.agents/reports/planner-reviewer-claude-01/plan-29924fd4.md b/.agents/reports/planner-reviewer-claude-01/plan-29924fd4.md new file mode 100644 index 0000000..e1d5951 --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/plan-29924fd4.md @@ -0,0 +1,368 @@ +# 🧭 Layout Engine 개선 계획 (Rev.2) — 결정론적 2×K 그리드 + +- **Planner**: `planner-reviewer-claude-01` +- **Job**: `8722045f` (Rev.1 = `29924fd4`) +- **개정 사유**: `creator-grok-01` 이의제기(`b907f997`) 수용 +- **검증**: BSP 시뮬레이터 + 격리 워크스페이스 실측(`w16`/`w17`, 정리 완료) + 코드 실측 + +--- + +## 0. Rev.1 대비 변경 요약 + +| # | 항목 | Rev.1 | **Rev.2** | +|---|---|---|---| +| **C-1** | 헤드리스 결정 규칙 | "홀짝 반전" (§6.1) | **홀짝 폐기.** GUI 와 **동일 결정표** 공유 (§4.2) | +| **C-2** | `max_columns` 배선 | 시그니처 기본값 2 | **`_env_int(default=2)` + `lib.sh --max-cols` 동시 적용** (§4.5) | +| **C-3** | GUI 채우기 규칙 | singleton 휴리스틱 | **열 페인 수 기반**(`max_rows` 일반화) (§4.1) | +| **C-4** | 궤적 표 | `(3,2)→(3,3)` (3행) | `max_rows=2` 와 **모순 해소** — 4페인에서 정지 (§4.1) | +| **C-5** | `.mam.env.example` | 미언급 | `MAM_MAX_PANE_COLS` 문서 기본값 갱신 대상에 추가 (§4.5) | + +**이의제기 3건 모두 인용(SUSTAINED)합니다.** C-3·C-4·C-5 는 이의제기가 드러낸 제 계획서의 추가 결함으로, 자진 정정합니다. + +§1~§3(근본 원인 분석·실측 증거)은 Rev.1 그대로 유효하므로 §11 에 요약만 남깁니다. + +--- + +## 1. C-1 인용 — 홀짝 반전은 n=3 에서 GUI 와 갈라진다 + +### 이의제기 검증 +현행 헤드리스(`layout.py:113-125`)는 `n % 2` 로 기하를 흉내 냅니다. Rev.1 §6.1 이 "반전"이라고만 적었으므로, 구현자가 문자 그대로 뒤집으면: + +| n | 홀짝 반전 결과 | GUI(Rev.2 §4.1) | 판정 | +|---|---|---|---| +| 1 | `right` | `right` | ✅ | +| 2 | `down` | `down` | ✅ | +| 3 | **`right`** | **`down`** | 🔴 **갈라짐 — 3열을 염** | +| 4 | `down` | `overflow` | 🔴 | + +0×0 에서는 열 그룹핑이 불가능하므로 잘못된 `right` 가 **GUI 보다 먼저, 더 조용히** 3열을 만듭니다. 이의제기가 정확합니다. + +### 근본 원인 +**열 채우기는 `n % 2` 로 표현되지 않습니다.** 패리티는 "직전에 무엇을 했는가"를 인코딩할 뿐, "각 열이 얼마나 찼는가"를 모릅니다. Rev.1 은 이를 "반전"이라는 한 단어로 넘겨 구현자에게 잘못된 자유도를 남겼습니다. + +### 기존 테스트가 고정 중인 옛 계약 +`tests/test_layout.py:380-414` `test_headless_max_columns_growth_guard` 는 현행 홀짝을 명시적으로 고정합니다: +```python +d2 = compute_2xk_layout(headless(2), max_columns=2) # right +d3 = compute_2xk_layout(headless(3), max_columns=2) # down +d5 = compute_2xk_layout(headless(5), max_columns=2) # down, despite cap +assert compute_2xk_layout(headless(4)).direction == "right" # 무제한일 때 +``` +`n=2 → right` 와 `n=4 → right` 단언은 Rev.2 계약과 **정면 충돌**하므로 W2 커밋에서 함께 갱신해야 합니다(§7.2). + +--- + +## 2. 핵심 설계 — 단일 결정표 (Single Decision Table) + +**GUI 분기와 헤드리스 분기는 같은 결정표를 쓴다.** 차이는 *상태를 어떻게 관측하는가*뿐이며, *무엇을 결정하는가*는 동일합니다. + +``` +상태: cols = 열별 페인 수 리스트, capacity = max_columns × max_rows +관측: GUI → x 좌표 그룹핑으로 cols 산출 + 헤드리스 → 생성 순서로 cols 추론 (§4.2) + +결정표 (공통): + ① len(cols) < max_columns 그리고 전고 페인 존재 → RIGHT (새 열) + ② 가장 적은 열의 페인 수 < max_rows → DOWN (그 열 채우기) + ③ 그 외 → OVERFLOW +``` + +이 표가 유일한 진실 원천이며, 두 분기는 이를 **호출만** 합니다. + +--- + +## 3. 궤적 (C-4 정정) + +`max_columns=2`, `max_rows=2` (capacity 4) 기준. n = **현재 페인 수**, 결정은 *다음* 페인용입니다. + +| n | 상태 `(a,b)` | 규칙 | 결정 | 결과 | +|---|---|---|---|---| +| 1 | `(1,-)` | ① | **`right`** on p1 | `(1,1)` | +| 2 | `(1,1)` | ② | **`down`** on 열1 최하단 | `(2,1)` | +| 3 | `(2,1)` | ② | **`down`** on 열2 최하단 | `(2,2)` ← **2×2 완성** | +| 4 | `(2,2)` | ③ | **`overflow`** | 새 워크스페이스 | + +> **Rev.1 정정**: Rev.1 §4.1 은 궤적을 `(3,2) → (3,3)` 까지 적었으나, 같은 문서 §4.4 가 `max_rows=2` 를 제안하여 **자기모순**이었습니다. Rev.2 는 `max_rows=2` 기준으로 4페인에서 정지합니다. `max_rows=3` 을 열면 궤적이 `(3,2) → (3,3)` 으로 자연히 연장되며, 그때는 §5 행 균등화가 선행되어야 합니다. + +--- + +## 4. 알고리즘 명세 + +### 4.1 GUI 분기 (C-3 — singleton 휴리스틱 폐기) + +현행 `fill_singleton_column`(`layout.py:147-159`)은 `len(col) == 1` 만 봅니다. 이는 **`max_rows=2` 에서만 우연히 맞고**, `max_rows=3` 에서는 `(2,2)` 상태에 singleton 이 없어 새 열을 시도하다 캡에 걸려 **조기 overflow** 합니다. 열 페인 수 기반으로 일반화합니다. + +```python +def _decide(cols, area_h, max_columns, max_rows, min_cols, min_rows): + # ① 새 열: 전고 페인이 있을 때만 (R-2) + if len(cols) < max_columns: + fh = _full_height_pane(cols[-1], area_h) + if fh is not None: + if fh.width > 0 and fh.width // 2 < min_cols: + return OVERFLOW("column_width_overflow", fh) + return RIGHT("new_column_right", fh) + # 전고 페인이 없으면 새 열을 열 수 없다 → ②로 폴백 + + # ② 가장 적은 열을 채운다 (동률이면 좌측 우선 — 결정론) + shortest = min(cols, key=lambda c: (len(c), c[0].x)) + if len(shortest) < max_rows: + bottom = shortest[-1] # y 정렬 후 최하단 + if min_rows > 0 and bottom.height > 0 and bottom.height // 2 < min_rows: + return OVERFLOW("row_height_overflow", bottom) + return DOWN("fill_column", bottom) + + # ③ + return OVERFLOW("grid_capacity_reached", cols[-1][0]) +``` + +`_full_height_pane` (R-2 처방): +```python +def _full_height_pane(col, area_h, tol=2): + """열 전체 높이를 점유하는 단일 페인. 없으면 None.""" + if len(col) != 1: + return None + return col[0] if (area_h <= 0 or abs(col[0].height - area_h) <= tol) else None +``` + +**동률 시 좌측 우선**(`(len(c), c[0].x)`)은 결정론 보장을 위한 필수 타이브레이커입니다. `min()` 은 첫 최소값을 반환하지만 `cols` 정렬이 바뀌면 결과가 흔들리므로 명시합니다. + +### 4.2 헤드리스 분기 (C-1 처방 — 생성 순서로 열 추론) + +0×0 에서도 **생성 순서가 열을 결정**합니다. `right` 후 `p1`=열1, `p2`=열2 이고, 이후 `down` 채우기는 열을 번갈아 갑니다. 따라서 인덱스 `i`(0-based)의 페인은 열 `i % max_columns` 에 속합니다. + +```python +def _decide_headless(panes, max_columns, max_rows): + n = len(panes) + if n >= max_columns * max_rows: + return OVERFLOW("grid_capacity_reached", panes[-1]) + if n < max_columns: + return RIGHT("new_column_right", panes[n - 1]) + return DOWN("fill_column", panes[n - max_columns]) +``` + +**타깃 선택 근거**: `panes[n - max_columns]` 는 다음에 채울 열의 **최하단 페인**입니다. + +| n | `n - max_columns` | 타깃 | 들어가는 열 | +|---|---|---|---| +| 2 | 0 | `p1` | 열1 → `(2,1)` ✅ | +| 3 | 1 | `p2` | 열2 → `(2,2)` ✅ | +| 4 | 2 | `p3` | 열1 (max_rows=3 일 때) ✅ | +| 5 | 3 | `p4` | 열2 ✅ | + +**주의**: 헤드리스에서 `default_anchor_id`(lib.sh 의 `--sample-pane`)를 타깃으로 쓰면 **안 됩니다.** lib.sh 는 워크스페이스의 *첫* 페인을 넘기므로, 그것을 계속 타깃하면 한 열만 깊어집니다. 현행 코드의 `anchor = default_anchor_id or panes[-1]` 는 이 경로에서 **제거**해야 하며, `default_anchor_id` 는 페인이 0개일 때의 폴백으로만 남깁니다. + +### 4.3 결정표 공유 강제 (구조적 보증) +두 분기가 갈라지지 않도록 `compute_2xk_layout` 은 **관측 → 공통 결정** 2단으로 재구성합니다: + +```python +def compute_2xk_layout(data, min_cols=15, min_rows=0, max_columns=2, max_rows=2, default_anchor_id=None): + panes = extract_panes(data) + if not panes: + return RIGHT("no_panes_default", default_anchor_id or "") + if all(p.width <= 0 or p.height <= 0 for p in panes): + return _decide_headless(panes, max_columns, max_rows) # ← 같은 표 + cols = _group_columns(panes) + return _decide(cols, _area_height(data, panes), max_columns, max_rows, min_cols, min_rows) +``` + +§7.2 의 parity 테스트가 이 공유를 **계약으로 고정**합니다. + +### 4.4 파라미터 요약 + +| 변수 | 현재 | Rev.2 | 근거 | +|---|---|---|---| +| `MAM_MAX_PANE_COLS` | unset(무제한) | **2** | "2×K" 의 2를 실제로 강제 | +| `MAM_MAX_PANE_ROWS` | 없음 | **2** (신규) | §5 균등화 전까지 보장 구간 | +| `MAM_MIN_PANE_COLS` | 15 | 유지 | 2열 상한 하에서 역할 축소 | +| `MAM_MIN_PANE_ROWS` | 0 | 유지 | 상동 | + +### 4.5 C-2 인용 — `max_columns` 배선 (실측 확인) + +이의제기의 부수 지적을 코드로 확인했습니다. + +```python +# layout.py:203 ← default= 없음 → env 미설정 시 None +parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS")) +# layout.py:217-223 ← 항상 전달 +decision = compute_2xk_layout(..., max_columns=args.max_cols, ...) +``` +`None` 이 **무조건 전달**되므로 시그니처 기본값 `2` 는 CLI 경로에서 **절대 적용되지 않습니다.** 그리고 `lib.sh:432` 는 `--max-cols` 를 **아예 넘기지 않습니다**(grep 결과 `lib.sh` 내 0건). + +즉 **lib.sh → layout.py 경로가 유일한 생산 경로인데, 거기서 캡이 영원히 `None`** 입니다. Rev.1 의 W4 는 프로덕션에 무효였습니다. + +**필수 3중 조치 (같은 커밋)**: +```python +# 1) layout.py:203 +parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS", default=2)) +parser.add_argument("--max-rows", type=int, default=_env_int("MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS", default=2)) +``` +```bash +# 2) lib.sh:432 +python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-15}" --min-rows "${MAM_MIN_PANE_ROWS:-0}" \ + --max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}" --sample-pane "$sample_pane" +``` +``` +# 3) .mam.env.example:146-148 (C-5) +#default: 2 ← 현재 "(unset -> no column cap)" 이고 예시가 =3 이라 이중으로 어긋남 +# MAM_MAX_PANE_COLS=2 +# (신규 블록) MAM_MAX_PANE_ROWS=2 +``` + +> **C-5 추가 발견**: `.mam.env.example:147` 은 기본값을 "(unset → no column cap)" 로, `:148` 예시는 `=3` 으로 적어 **문서 자체가 이미 불일치**합니다. Rev.2 값으로 양쪽을 함께 정정하십시오. + +**W4 회귀 가드** (§7.2에 포함): +```python +def test_max_cols_default_reaches_cli_path(): ... # env 없이 --json 실행 시 4페인에서 overflow +def test_lib_sh_passes_max_cols_and_rows(): ... # lib.sh 소스 문자열 가드 +``` + +--- + +## 5. 행 균등화 — `max_rows ≥ 3` 의 전제조건 + +BSP 단일 분할은 **한 페인만** 이등분하므로 형제 높이가 안 바뀝니다. 높이 78, 2페인(각 39)인 열에 1개를 더하면 `{39, 19, 20}` 이 되고 `--ratio` 를 써도 형제는 그대로입니다. **3행 균등은 분할만으로 불가능**하며 `herdr pane resize --pane --direction up|down --amount ` 정규화 패스가 필요합니다. + +따라서 **`max_rows` 기본값 2 는 임의 선택이 아니라 "균등을 보장할 수 있는 최대치"** 입니다. §6 W6 완료 전에는 3행을 열지 마십시오(D2). + +--- + +## 6. WBS + +| 단계 | 작업 | 파일 | 비고 | +|---|---|---|---| +| **W1** | `_full_height_pane`, `_group_columns` 추출 | `layout.py` | | +| **W2** | **공통 결정표 + GUI/헤드리스 양 분기 동시 전환** | `layout.py` | **C-1. "1줄" 아님** | +| **W3** | 헤드리스 타깃 `panes[n - max_columns]`, anchor 오용 제거 | `layout.py` | C-1 | +| **W4** | `max_cols`/`max_rows` **3중 배선** | `layout.py`, `lib.sh:432`, `.mam.env.example` | **C-2·C-5** | +| **W5** | `grid_health()` + `--health` | `layout.py` | 진단 | +| **W6** | ratio 정규화 리컨사일러 | 신규 `lib_py/layout_repair.py` | §5 | +| **W7** | 리컨사일러 배선 | `create/resume/reconcile` | | +| **W8** | 테스트 (§7) | `tests/test_layout.py` | | + +### 6.1 W2 범위 정정 (C-1) +Rev.1 은 W2 를 **"핵심 1줄"** 이라 적었습니다. **이 표현을 철회합니다.** W2 는 최소한 다음을 **하나의 커밋**에 포함해야 합니다: + +1. GUI 분기를 §4.1 결정표로 교체 +2. 헤드리스 분기를 §4.2 결정표로 교체 (**홀짝 로직 삭제**) +3. 두 분기가 같은 `_decide*` 계층을 호출하도록 구조 정리 (§4.3) +4. `test_headless_max_columns_growth_guard` 등 옛 계약 테스트 갱신 + +**분리 커밋 금지**: GUI 만 바꾸고 헤드리스를 남기면 §1 표의 n=3 갈라짐이 그대로 생산에 들어갑니다. + +--- + +## 7. 테스트 계획 + +### 7.1 누적 시퀀스 가드 (Rev.1 유지 — 최중요) +단일 결정만 단언하는 현행 방식은 R-1 을 통과시켰습니다. **BSP 시뮬레이터를 테스트 헬퍼로 승격**합니다. +```python +def test_four_panes_form_clean_2x2(): + panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] + for _ in range(3): + d = compute_2xk_layout(_payload(panes)) + assert not d.is_overflow + panes = _bsp_split(panes, d.target_pane_id, d.direction) + assert len(panes) == 4 + assert max(p["w"] for p in panes) - min(p["w"] for p in panes) <= 2 + assert max(p["h"] for p in panes) - min(p["h"] for p in panes) <= 2 + +def test_fifth_pane_overflows(): + ... # capacity 4 → 4페인 상태에서 overflow +``` + +### 7.2 GUI ↔ 헤드리스 parity (C-1 처방 — 확장) +Rev.1 은 `n=1` 만 단언했습니다. 이의제기대로 **전 구간**을 단언합니다: +```python +@pytest.mark.parametrize("n,expected", [(1,"right"), (2,"down"), (3,"down"), (4,"overflow")]) +def test_headless_matches_gui_decision(n, expected): + hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) + assert hl.direction == expected, f"headless n={n}" + +def test_headless_gui_direction_parity_full_sequence(): + """같은 n 에서 두 분기의 direction 이 항상 일치한다.""" + panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] + for n in range(1, 5): + gui = compute_2xk_layout(_payload(panes), max_columns=2, max_rows=2) + hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) + assert gui.direction == hl.direction, f"divergence at n={n}" + if gui.is_overflow: + break + panes = _bsp_split(panes, gui.target_pane_id, gui.direction) + +def test_headless_fills_alternating_columns(): + """C-1: n=2 는 p1, n=3 은 p2 를 타깃해야 한 열만 깊어지지 않는다.""" + assert compute_2xk_layout(_headless(2), max_columns=2, max_rows=2).target_pane_id == "p1" + assert compute_2xk_layout(_headless(3), max_columns=2, max_rows=2).target_pane_id == "p2" + +def test_headless_ignores_sample_pane_anchor(): + """§4.2: --sample-pane 이 채우기 타깃을 오염시키지 않는다.""" + d = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2, default_anchor_id="p1") + assert d.target_pane_id == "p2" +``` + +### 7.3 갱신 대상 기존 테스트 +| 테스트 | 충돌 단언 | 조치 | +|---|---|---| +| `test_headless_max_columns_growth_guard:399` | `n=2 → right` | → `down` | +| 〃 `:409` | `n=5 → down` (캡 무시) | capacity 규칙으로 재작성 | +| 〃 `:414` | `n=4 무제한 → right` | 기본 캡 2 하에서 재정의 | +| `test_headless_0x0_transitions:142` | 홀짝 전제 | 전면 재작성 | +| `test_j1*` (min_cols/rows) | 영향 없음(명시 전달) | 유지 | + +### 7.4 W4 배선 가드 +§4.5 의 두 테스트. **env 미설정 상태**에서 CLI 경로가 실제로 캡을 적용하는지 확인하는 것이 핵심입니다. + +--- + +## 8. 리스크 + +| 리스크 | 영향 | 완화 | +|---|---|---| +| **GUI/헤드리스 분리 커밋** | n=3 갈라짐이 조용히 생산 진입 | §6.1 단일 커밋 강제 + §7.2 parity 테스트 | +| **W4 배선 누락** | 캡이 `None` 으로 남아 Rev.2 전체가 무효 | §4.5 3중 조치 + §7.4 가드 | +| 헤드리스 anchor 오용 | 한 열만 깊어짐 | §4.2 주의 + `test_headless_ignores_sample_pane_anchor` | +| 기존 테스트 대량 실패 | 계약 변경이라 불가피 | §7.3 목록대로 갱신 | +| capacity 4 로 워크스페이스 증가 | 5+ 에이전트에서 워크스페이스 수↑ | 의도된 트레이드오프(D1) | + +--- + +## 9. 결정 필요 사항 + +| ID | 항목 | 권장 | +|---|---|---| +| **D1** | `max_columns=2`, `max_rows=2` (capacity 4) 수용 | **수용** — 4 에이전트 2×2 목표와 일치 | +| **D2** | 3행 개방 시점 | W6 정규화 완료 후 | +| **D3** | 리컨사일러 자동 재배치 범위 | ratio 정규화까지만 자동 | +| **D4** | 기존 왜곡 워크스페이스 | 진단만, 복구 수동 | + +--- + +## 10. 이의제기 대응 정리 + +| 이의 | 판정 | 반영 | +|---|---|---| +| 홀짝 반전 시 n=3 갈라짐 | ✅ **인용** | §1, §4.2, §6.1, §7.2 | +| `max_columns` 배선 누락 | ✅ **인용** (실측 확인) | §4.5, §7.4 | +| parity 테스트가 n=1 만 단언 | ✅ **인용** | §7.2 전 구간 파라미터화 | +| *(자진 정정)* singleton 휴리스틱 비일반성 | — | §4.1 | +| *(자진 정정)* 궤적 표 ↔ `max_rows=2` 모순 | — | §3 | +| *(자진 발견)* `.mam.env.example` 자체 불일치 | — | §4.5 C-5 | + +--- + +## 11. Rev.1 근거 요약 (변경 없음) + +- **herdr = 엄격 BSP**: `split` 은 대상 페인 rect 만 이등분(격리 `w16` 실측). +- **R-1**: `down` 우선 시 하단 페인이 전폭으로 남아 **2×2 도달 불가**. 시뮬레이션상 N=4 에서 widths `{138,139,277}`, heights `{19,20,39}`. +- **R-2**: 전고 페인이 없으면 `right` 는 반쪽 열만 생성. +- **해법 실증**: `right` 우선 → `down` ×2 → widths `[138,139]`, heights `[39]` 완전 균등(격리 `w17` 실측). 트리 구조가 살아있는 `w15` 와 동일. + +--- + +## 12. 결론 + +이의제기 3건을 모두 인용하며, 그 과정에서 제 계획서의 추가 결함 3건(C-3·C-4·C-5)을 자진 정정했습니다. + +Rev.1 의 가장 위험한 표현은 **"핵심 1줄"** 이었습니다. 근본 원인 진단은 옳았으나, 처방의 범위를 과소 표기하여 구현자가 GUI 만 고치고 헤드리스를 홀짝 반전으로 처리할 여지를 남겼습니다. Rev.2 는 이를 **단일 결정표 공유**로 구조적으로 차단하고(§4.3), parity 테스트로 계약을 고정합니다(§7.2). + +`max_columns` 배선 지적은 특히 중요합니다 — 이것이 없으면 **Rev.2 전체가 프로덕션에서 무효**입니다. lib.sh 가 `--max-cols` 를 넘기지 않고 `_env_int` 가 `None` 을 반환하는 이중 누락이라, 시그니처 기본값만 바꾸는 수정은 테스트만 통과하고 실사용에서는 아무 효과가 없었을 것입니다. diff --git a/.agents/reports/planner-reviewer-claude-01/report-5e43d80f.md b/.agents/reports/planner-reviewer-claude-01/report-5e43d80f.md new file mode 100644 index 0000000..2acc088 --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-5e43d80f.md @@ -0,0 +1,183 @@ +# 🔍 Cross Review — Job 5e43d80f: Layout Engine 2×K 구현 + +- **Reviewer**: `planner-reviewer-claude-01` +- **대상**: W1~W5 (`layout.py`, `lib.sh:435`, `.mam.env.example`, 테스트 3파일) +- **검증**: BSP 시뮬레이터 실행 + 헤드리스 궤적 실행 + **pytest 전체 393건 실행** + +> **이해충돌 고지**: 본 구현의 사양(`layout_engine_improvement_plan.md` Rev.2)은 제가 Planner 로 작성했습니다. 아래는 **타인이 작성한 코드가 그 사양을 충족하는가**에 대한 검증이며, 사양 자체의 타당성에 대한 독립 검증이 아닙니다. §4 의 F-1 은 실제로 **제 사양의 결함**이며 그렇게 명시합니다. + +--- + +## 1. 핵심 목표 달성 — 실측 확인 + +BSP 세분할 시뮬레이터로 N=1→5 궤적을 실행했습니다. + +``` +N=1 [p1 277x78] +N=2 split right on p1 (new_column_right) + p1 138x78 / p2 139x78 widths=[138,139] heights=[78] balanced=True +N=3 split down on p1 (fill_column) + p1 138x39 / p3 138x39 / p2 139x78 (전이 상태 — 정상) +N=4 split down on p2 (fill_column) + p1 138x39 / p3 138x39 / p2 139x39 / p4 139x39 + widths=[138,139] heights=[39] balanced=True ← 🎯 깨끗한 2×2 +N=5 OVERFLOW (grid_capacity_reached) → 새 워크스페이스 +``` + +**4 에이전트 2×2 목표 달성.** Rev.1 이 진단한 R-1 왜곡(N=4 에서 widths `{138,139,277}`, heights `{19,20,39}`)이 완전히 해소되었습니다. + +### 1.1 GUI ↔ 헤드리스 패리티 (C-1 처방 검증) +``` +headless n=1: right target=p1 reason=new_column_right +headless n=2: down target=p1 reason=fill_column +headless n=3: down target=p2 reason=fill_column +headless n=4: overflow target=p4 reason=grid_capacity_reached +``` +GUI 와 **방향·순서가 완전히 일치**하며, 타깃도 `p1 → p2` 로 **열을 번갈아** 갑니다(§4.2 `panes[n - max_columns]` 명세대로). 홀짝(`n % 2`) 로직은 완전히 제거되었습니다. `creator-grok-01` 이 제기했던 "n=3 갈라짐"이 원천 차단되었습니다. + +--- + +## 2. W1~W5 항목별 검증 + +| W | 항목 | 상태 | +|---|---|---| +| **W1** | 단일 결정표 공유 | ✅ `_decide` / `_decide_headless` 분리, `compute_2xk_layout` 은 관측→위임 2단 | +| **W2** | N=1 → `right` | ✅ `_full_height_pane` 경유 `new_column_right` | +| **W3** | `_full_height_pane` | ✅ `len(col)!=1 → None`, `area_h<=0` 폴백, `tol=2` | +| **W4** | 캡 3중 배선 | ✅ **전부** — 아래 §2.1 | +| **W5** | 테스트 | ✅ 사양의 8개 테스트 전건 구현 | + +### 2.1 W4 — Rev.2 가 지적한 이중 누락이 모두 해소됨 +```python +# layout.py:235-236 ← _env_int 에 default 부여 (이전엔 None 반환) +--max-cols default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS", default=2) +--max-rows default=_env_int("MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS", default=2) +``` +```bash +# lib.sh:435 ← 이전엔 --max-cols 를 아예 넘기지 않았음 +--max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}" +``` +``` +# .mam.env.example:147-153 ← C-5 문서 자체 불일치("unset -> no cap" vs "=3")도 정정 +#default: 2 / MAM_MAX_PANE_COLS=2 + MAM_MAX_PANE_ROWS=2 신규 블록 +``` +추가로 `compute_2xk_layout` 진입부에 `max_columns is None → 2` 방어가 들어가 **네 번째 경로**까지 막았습니다. 사양보다 견고합니다. + +`test_max_cols_default_reaches_cli_path` 와 `test_lib_sh_passes_max_cols_and_rows` 가 이 배선을 계약으로 고정합니다 — Rev.2 가 "이게 없으면 전체가 프로덕션에서 무효"라고 경고한 지점이라 특히 중요합니다. + +### 2.2 기존 테스트 갱신 처리 +| 테스트 | 처리 | +|---|---| +| `test_1_pane_split_down` → `test_1_pane_split_right` | ✅ 개명 + 계약 갱신 | +| `test_2_panes_to_3_panes_new_column_right` → `test_2_panes_fill_left_column_down` | ✅ | +| `test_4_panes_to_5_panes_new_column` → `..._overflows_at_capacity` | ✅ | +| `test_headless_max_columns_growth_guard` | ✅ 옛 홀짝 단언(`n=2→right`, `headless_odd_down`) 전면 재작성 | +| `test_b19_headless_layout_does_not_overflow` | ✅ tall 케이스 재해석 + **wide-tall 케이스 신설로 커버리지 보존** | + +`test_b19` 처리가 특히 좋습니다 — 단언만 뒤집지 않고 비오버플로 경로를 검증하는 새 픽스처를 추가해 커버리지를 유지했습니다. + +### 2.3 테스트 실행 +``` +pytest tests/ -q → 393 passed in 511.60s +``` +**실패 0건.** (직전 리뷰에서 관측된 `test_d23` nats 태그 불일치도 해소되었습니다.) + +--- + +## 3. 🟡 F-2 — `MAX_*=0` 이 "무제한"이 아니라 "전면 차단"입니다 + +`_env_int` 는 J-1 계약에 따라 명시적 `0` 을 보존합니다. 그 결과: + +| 설정 | 실측 결과 | +|---|---| +| `max_columns=0, max_rows=2` | `down / fill_column` | +| `max_columns=2, max_rows=0` | `right` → 이후 `overflow` | +| `max_columns=0, max_rows=0` | **`overflow / grid_capacity_reached` (즉시)** | +| 헤드리스, 둘 중 하나라도 0 | `n >= 0` 이 항상 참 → **영구 overflow** | + +문제는 **같은 설정 파일 안의 의미 충돌**입니다: +``` +# .mam.env.example +# MAM_MIN_PANE_ROWS=0 ← "Set to 0 to disable vertical row constraints" +# MAM_MAX_PANE_ROWS=2 ← 0 을 넣으면 "용량 0" = 모든 세션이 새 워크스페이스 +``` +`MIN_*=0` 이 "제약 해제"를 뜻하므로, 운영자가 `MAX_*=0` 을 "상한 없음"으로 읽는 것은 자연스럽습니다. 그러나 실제로는 **에이전트마다 워크스페이스가 무한 생성**됩니다. + +**개선 방향**: +```python +if max_columns is None or max_columns <= 0: + max_columns = 2 # 또는 '무제한' 의도라면 sys.maxsize +if max_rows is None or max_rows <= 0: + max_rows = 2 +``` +`.mam.env.example` 에도 `0 은 허용되지 않습니다(최솟값 1)` 한 줄을 덧붙이십시오. 비차단이나 오설정 시 피해가 크고 되돌리기 어렵습니다(생성된 워크스페이스가 남음). + +--- + +## 4. 🟠 F-1 — 세로 전용 적층 능력이 사라졌습니다 (**제 사양의 결함**) + +### 현상 (실측) +`min_cols=60` 에서 단일 페인 폭별 결정: + +| 폭 × 높이 | 결과 | +|---|---| +| 80×60 | `overflow / column_width_overflow` | +| 100×60 | `overflow` | +| 119×60 | `overflow` | +| 120×60 | `right` | + +**폭 120 미만이면 N=1 에서 즉시 오버플로**합니다. 그러나 80×60 을 세로로 쌓으면 `80×30` 페인 2개가 되고, **두 페인 모두 폭 80 ≥ min_cols 60 을 만족**합니다. 즉 **사용 가능한 배치를 거부하고 새 워크스페이스를 만듭니다.** + +### 원인 — 폭 게이트가 폴스루하지 않음 +```python +if len(cols) < max_columns: + fh = _full_height_pane(cols[-1], area_h) + if fh is not None: + if fh.width > 0 and fh.width // 2 < min_cols: + return _overflow("column_width_overflow", fh) # ← 즉시 반환 + return _right("new_column_right", fh) +# ② 세로 채우기에 도달하지 못함 +``` +새 엔진은 **1열 × K행 배치를 구조적으로 만들 수 없습니다.** 구 엔진의 `single_pane_split_down` 이 담당하던 경로가 사라졌습니다. + +### 책임 소재 +**이것은 구현 결함이 아니라 제 사양의 결함입니다.** Rev.2 §4.1 의사코드가 정확히 `return OVERFLOW("column_width_overflow", fh)` 로 적혀 있었고, 구현은 그대로 따랐습니다. 폭 부족 시 세로 폴백을 명시하지 않은 것은 제 누락입니다. + +### 실무 영향 +기본값 `min_cols=15` 에서는 폭 30 미만이어야 발동하므로 **사실상 도달 불가**합니다. 다만 `MAM_MIN_PANE_COLS` 기본값은 60 → 40 → 15 로 변해 왔고, `.mam.env` 는 **gitignore 대상이라 자동 마이그레이션되지 않습니다.** 구 설정(`=60`)을 지닌 기존 설치는 120칸 미만 터미널에서 **에이전트마다 워크스페이스가 하나씩** 생기게 됩니다. + +### 개선 방향 (구체) +폭 게이트를 **폴스루**로 바꿉니다: +```python +if len(cols) < max_columns: + fh = _full_height_pane(cols[-1], area_h) + if fh is not None and not (fh.width > 0 and fh.width // 2 < min_cols): + return _right("new_column_right", fh) + # 폭이 새 열을 감당하지 못하면 ②(세로 채우기)로 내려간다 +``` +이렇게 하면 80×60/min_cols=60 은 `down / fill_column` → `80×30` 2개가 되고, 폭·행이 모두 소진된 뒤에야 ③에서 오버플로합니다. 회귀 가드: +```python +def test_narrow_terminal_falls_back_to_vertical_stacking(): + d = compute_2xk_layout(_one(80, 60), min_cols=60) + assert d.direction == "down" and not d.is_overflow +``` +**주의**: 이 변경은 `test_b19` 의 tall 케이스 단언을 다시 뒤집습니다(현재 `overflow` → `down`). 원래 그 테스트가 지키던 계약이 바로 이 세로 폴백이었으므로, 사실상 **원복**입니다. + +--- + +## 5. 판정 + +| 항목 | 판정 | +|---|---| +| W1~W5 사양 충족 | ✅ 전건. W4 는 사양보다 견고 | +| 4 에이전트 2×2 | ✅ 시뮬레이터 실측 | +| GUI/헤드리스 패리티 | ✅ 방향·타깃 모두 일치 | +| 전체 테스트 | ✅ **393 passed, 0 failed** | +| F-1 세로 폴백 상실 | 🟠 **제 사양 누락** — 후속 수정 | +| F-2 `MAX_*=0` 함정 | 🟡 후속 수정 | + +구현은 승인된 사양을 **정확히, 그리고 일부는 더 견고하게** 충족했으며 핵심 목표가 실측으로 증명되었습니다. F-1·F-2 는 이번 변경이 만든 새 결함이 아니라 **사양의 미비**로, 각각 3~5줄 수정으로 해소됩니다. 설계 재작업 사유가 아니므로 `[ESCALATE: PLANNER]` 는 부여하지 않습니다. + +**후속 잡 권고**: F-1(세로 폴백) + F-2(0 값 클램프) 를 묶어 한 커밋으로. F-1 은 구 `.mam.env` 를 지닌 기존 설치에 실제 영향이 있으므로 우선순위가 높습니다. + +[VERDICT: PASS] diff --git a/.agents/reports/planner-reviewer-claude-01/report-e4631ccd.md b/.agents/reports/planner-reviewer-claude-01/report-e4631ccd.md new file mode 100644 index 0000000..d9a1b0e --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-e4631ccd.md @@ -0,0 +1,142 @@ +# 🔍 Cross Review — Job e4631ccd + +- **Reviewer**: `planner-reviewer-claude-01` (role: `planner,reviewer`) +- **브리프 목표**: Layout Engine 분석 + 개선 계획 수립 +- **검증**: 워킹트리 실측 + `git log` 대조 + pytest 83건 실행 + +--- + +## 0. ⚠️ 먼저 밝혀야 할 두 가지 + +### 0.1 자기 리뷰 이해충돌 (Independence Conflict) + +브리프의 산출물인 `.agents/reports/layout_engine_improvement_plan.md` 는 **제가 Planner 로서 직접 작성한 문서**입니다(job `29924fd4` Rev.1 → `8722045f` Rev.2). + +MULTI_AGENT_RULES §1·§3 의 리뷰 루프는 **작성자와 검증자의 분리**로 신호를 만듭니다. 제가 제 문서에 `PASS` 를 찍으면 그 신호는 **0** 입니다. 따라서: + +> **본 리뷰는 계획서 자체의 타당성을 독립 검증하지 않습니다.** +> 계획서에 대한 독립 검증이 필요하다면 `reviewer-cline-01` 또는 `creator-grok-01` 에게 배정하십시오. 실제로 Rev.1 → Rev.2 개정은 `creator-grok-01` 의 이의제기(`b907f997`)로 이루어졌고, 그것이 이 문서가 받은 유일한 독립 검증입니다. + +아래 판정은 **제가 작성하지 않은 부분** — 즉 누적 diff — 으로 범위를 한정합니다. + +### 0.2 diff 에 레이아웃 엔진 변경이 **한 줄도 없습니다** + +브리프의 작업 목표는 레이아웃 엔진인데, 리뷰 대상 diff 는 전부 grok 에이전트 작업입니다. + +``` +$ git diff --stat + .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh | 4 ++-- + .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +$ git log --oneline -3 -- .agents/skills/lib_py/layout.py +f3ac68f feat(layout): relax MAM_MIN_PANE_COLS to 15 ... ← 최신. 계획 수립 이전 커밋 +``` + +`layout.py` 는 계획서가 지목한 상태 그대로입니다 — **미구현 확인**: + +| 계획서 항목 | 현재 코드 | 상태 | +|---|---|---| +| `max_columns` 기본 2 | `layout.py:75` `max_columns: Optional[int] = None` | ❌ 미적용 | +| `max_rows` 신설 | 파라미터 자체 없음 | ❌ 미적용 | +| 홀짝 폐기 | `layout.py:115` `if n % 2 == 1:` 잔존 | ❌ 미적용 | +| `headless_odd_down`/`headless_even_right` 제거 | `:120`, `:125` 잔존 | ❌ 미적용 | +| `_full_height_pane` (R-2) | 부재 | ❌ 미적용 | +| `right` 우선 (R-1 핵심) | `:102` 여전히 `single_pane_split_down` | ❌ 미적용 | + +**따라서 본 Verdict 는 레이아웃 엔진을 인증하지 않습니다.** 4 에이전트 왜곡(R-1)은 현재도 그대로 재현됩니다. + +--- + +## 1. 실제 리뷰 대상 — grok `--agent` 검증 3줄 + +### 1.1 변경 내용 +```diff +# resolve_session_id.sh:39-40 +- claude|agy|hermes|cline) ;; +- *) echo "ERROR: --agent must be claude or agy or hermes or cline" >&2; exit 2 ;; ++ claude|agy|hermes|cline|grok) ;; ++ *) echo "ERROR: --agent must be claude, agy, hermes, cline, or grok" >&2; exit 2 ;; + +# resume_session.sh:45 +- claude|agy|hermes|cline) ;; ++ claude|agy|hermes|cline|grok) ;; +``` + +### 1.2 정합성 검증 — 통과 + +두 스크립트가 grok 을 **받은 뒤 실제로 동작하는지** 하류 경로를 전수 확인했습니다: + +| 하류 의존 | 상태 | +|---|---| +| `resume_session.sh:112` CMD_FULL 폴백 `case` | ✅ `grok) ... --resume $UUID --permission-mode bypassPermissions` 존재 | +| `resume_session.sh:88` 바이너리 해석 | ✅ `else` 분기가 `command -v "$AGENT"` 로 grok 처리 | +| `resolve_session_id.sh` → `find_workspace_uuid` → `workspace_uuid.py:11` `OWN_KEY` | ✅ `'grok': 'grok_session_id_own'` 존재 | +| 〃 `workspace_uuid.py:33` `running_ids` 수집 | ✅ `grok_session_id_own` 포함 | +| `registry.py:9,16` 어댑터 등록 | ✅ | +| `verify_session.py` / `atomic_yaml.py` / `reconcile.sh` / `stop_session.sh` / `orc_onboard.sh` / `status.sh` | ✅ 전부 grok 포함 | + +**검증 `case` 만 열고 하류를 빠뜨리는 전형적 결함은 없습니다.** 이 3줄이 grok 통합의 마지막 구멍을 메웁니다. + +> 이는 제가 grok 계획서(`b8872c34`)에서 S-15/S-16/S-18 로 지목했던 지점들이며, 모두 반영되어 있음을 확인했습니다. + +### 1.3 테스트 +``` +pytest tests/test_a4_adapter_contract.py tests/test_tier1_unit.py tests/test_loop_cli.py -q +→ 83 passed in 13.77s +``` + +--- + +## 2. 🟡 N-1 — 같은 파일 안에서 usage 문자열이 갱신되지 않았습니다 + +`resolve_session_id.sh` 는 이번 diff 로 `:39` 의 `case` 와 `:40` 의 에러 문구를 갱신했지만, **같은 파일 `:4`·`:16` 의 usage 문자열은 4개 그대로**입니다. + +``` +resolve_session_id.sh:4 # bash resolve_session_id.sh --workspace --agent +resolve_session_id.sh:16 Usage: $0 --workspace --agent [--session ] +resolve_session_id.sh:39 claude|agy|hermes|cline|grok) ;; ← 이번에 갱신 +``` + +사용자가 `--help` 로 보는 목록과 파서가 받는 목록이 **어긋납니다**. grok 은 유효하지만 도움말은 존재를 부정합니다. + +동일 패턴이 다른 스크립트에도 남아 있습니다(실행 경로 아닌 문자열만): + +| 파일 | 행 | +|---|---| +| `resolve_session_id.sh` | 4, 16 | +| `resume_session.sh` | 12 | +| `create_session.sh` | 4, 29, 33 | +| `stop_session.sh` | 4, 15, 44, 49 | +| `update_yaml_resumed.sh` | 7, 14 | + +**대조적으로** `create_session.sh:94,213` 과 `stop_session.sh:98` 의 **에러 문구**는 이미 grok 을 포함하고, `SKILL.md` 들도 갱신되어 있습니다. 즉 **usage/주석 헤더만 일괄 누락**된 상태입니다. + +**개선 방향**: 12개 문자열을 `` 으로 일괄 치환. 실행 동작에 영향이 없어 차단하지 않으나, 이번 diff 가 건드린 파일 안에서 발생한 불일치이므로 같은 커밋에서 정리하는 것이 자연스럽습니다. + +> 근본적으로는 grok 계획서 §2.1 에서 권고한 **레지스트리 기반 목록 생성**(`all_agent_names()`)으로 해소될 문제입니다. 현재 이 목록이 20곳 이상에 문자열로 복제되어 있어, 에이전트를 추가할 때마다 일부가 반드시 누락됩니다. 후속 잡으로 등록을 권고합니다. + +--- + +## 3. 판정 근거 + +| 대상 | 판정 | +|---|---| +| grok `--agent` 검증 3줄 | ✅ 정확. 하류 경로 전수 확인, 83건 테스트 통과 | +| usage 문자열 (N-1) | 🟡 비차단 지적 | +| **레이아웃 엔진** | ⬜ **미구현 — 본 Verdict 의 인증 대상 아님** | +| **계획서 자체** | ⬜ **자기 저작 — 본 Verdict 의 인증 대상 아님** | + +diff 에 포함된 변경은 정확하고 완결적이며 회귀가 없습니다. 결함이 없는 작업을 `NOT PASS` 로 막을 이유가 없으므로 **PASS** 를 부여하되, **위 두 항목이 인증 범위 밖임을 Verdict 의 일부로 명시**합니다. + +설계 변경이나 재계획이 필요한 사안은 없습니다(계획서는 이미 Rev.2 로 개정 완료). 따라서 `[ESCALATE: PLANNER]` 는 부여하지 않습니다. + +--- + +## 4. 후속 권고 + +1. **레이아웃 엔진 구현 잡을 별도로 발주하십시오.** 계획서 §6 WBS(W1~W8)가 준비되어 있고, §6.1 이 "GUI·헤드리스 단일 커밋" 을 강제합니다. 현재 R-1 왜곡은 그대로 살아 있습니다. +2. **계획서 독립 리뷰**는 저 아닌 세션에 배정하십시오(§0.1). +3. **N-1 문자열 12곳** 일괄 정리 + 레지스트리 기반 목록화 후속 잡. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-cline-01/report-a45784f2.md b/.agents/reports/reviewer-cline-01/report-a45784f2.md new file mode 100644 index 0000000..3f52362 --- /dev/null +++ b/.agents/reports/reviewer-cline-01/report-a45784f2.md @@ -0,0 +1,103 @@ +# Code Review — Job `a45784f2`: Layout Engine Implementation (W1–W5) + +- **Reviewer**: cline (session: `herdr:reviewer-cline-01`) +- **Subject**: Implement the Layout Engine improvements per approved plan `.agents/reports/layout_engine_improvement_plan.md` (Rev.2) — W1–W5. +- **Scope reviewed**: cumulative working-tree diff (`git diff`), focusing on the implementation deliverable (W1–W5). The plan.md document itself was already reviewed and PASSED in the prior job (`a9de9b22`); this review focuses on the **code**. +- **Review axes**: lint, operability, loss/omission. + +--- + +## 1. Changeset (actual `git diff`, authoritative) + +| File | Change | W# | +|---|---|---| +| `.agents/skills/lib_py/layout.py` | Refactor `compute_2xk_layout` into shared decision table (`_decide`/`_decide_headless`); add `_full_height_pane`, `_group_columns`, `_area_height`, helper constructors; defaults `max_columns=2`/`max_rows=2` + `is None` guard; CLI `_env_int(default=2)` for `--max-cols`/`--max-rows`. | W1–W4 | +| `.agents/skills/lib.sh` (l.~435) | Add `--max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}"` to the `python3 -m lib_py.layout` invocation. | W4 | +| `.mam.env.example` (l.144–153) | Replace `#default: (unset -> no column cap)` with `#default: 2`; set `# MAM_MAX_PANE_COLS=2`; add `# MAM_MAX_PANE_ROWS=2` block with resize-normalisation warning. | W4 | +| `tests/test_layout.py` | Rename/adapt tests to new contract; add `test_cli_max_cols_flag_triggers_overflow`, `test_env_max_cols_applies_without_flag`, `test_lib_sh_passes_max_cols_and_rows`, CLI-default overflow test. | W5 | +| `tests/test_tier1_unit.py` | Update `test_layout_default_min_cols_15_in_tier1` (1-pane payloads) and `test_layout_single_workspace_54x23_compact_tiling_tier1` (N=1→right, N=2→down, N=3→down→p2, N=4→overflow/grid_capacity_reached). | W5 | +| `tests/test_b19_headless_reconcile_fixes.py` | Adapt `test_bug2` to W2 (N=1 always right; tall-but-narrow→overflow; tall-and-wide→right). | W2/W5 | + +> Note: `resolve_session_id.sh` / `resume_session.sh` (`grok` allowlist) are also in the working tree but belong to the agent-onboarding concern, not W1–W5; reviewed and PASSED in prior job `a9de9b22`, unchanged here. + +--- + +## 2. Work-Item Verification + +### W1 — Single decision table (GUI & headless) ✅ +`_decide` (GUI, geometry-grouped columns) and `_decide_headless` (creation-order occupancy) implement the **same** three-step table: +1. open a new column → `right` (`new_column_right`); +2. fill the shortest/under-filled column → `down` (`fill_column`); +3. capacity reached → `overflow` (`grid_capacity_reached`). + +The two paths differ only in **observation**, never in **decision** — exactly the plan §2 design. Helpers (`_right/_down/_overflow/_pane_id`) are single-responsibility and well-typed; no parity (`n % 2`) logic remains (grep for the old `single_pane_split_down`/`headless_odd_down` reasons returns empty — fully purged). + +### W2 — `single_pane_split_right` ✅ +N=1 → `right` in both modes: +- GUI `_decide` ①: `len(cols) < max_columns` and `_full_height_pane(cols[-1])` is the lone full-height pane → `new_column_right`. +- Headless `_decide_headless`: `n(=1) < max_columns(=2)` → `new_column_right`, target `panes[0]`. + +Old `single_pane_split_down` reason string is gone; `test_b19` correctly adapted. + +### W3 — `_full_height_pane` ✅ +`layout.py:91-97`: returns the pane only when `len(col)==1` and `|height − area_h| ≤ tol(=2)`, else `None`. Used in `_decide` ① to block opening a new column from a half-height pane — preventing the half-height column split the plan calls out (R-2). +### W4 — `max_columns=2` / `max_rows=2` triple wiring + env doc ✅ (central risk closed) +The plan's pivotal "double omission" (lib.sh never passed `--max-cols`; `_env_int` returned `None`) is **fully resolved** — now four overlapping defaults guarantee a 2×2 cap: +1. env `MAM_MAX_PANE_COLS`/`MAM_MAX_PANE_ROWS`; +2. shell `${VAR:-2}` fallback at `lib.sh:435`; +3. argparse `_env_int(..., default=2)` for `--max-cols`/`--max-rows` (`layout.py:235-236`); +4. signature `=2` + `if … is None: … = 2` guard (`layout.py:182-193`). + +`.mam.env.example` updated (the plan's C-5 inconsistency fixed: no more "unset → no column cap"); new `MAM_MAX_PANE_ROWS=2` block with "do not raise until a resize-normalisation pass exists" guidance. + +Verified through **three** integration tests exercising the real CLI entrypoint: +- `test_cli_max_cols_flag_triggers_overflow` — `--max-cols 2` reaches `compute_2xk_layout` → `grid_capacity_reached`. +- `test_env_max_cols_applies_without_flag` — `MAM_MAX_PANE_COLS=2` honored with **no flag** (the precise "double omission" scenario, now closed). +- CLI-default test (no flag, no env) → `grid_capacity_reached`, exercising the argparse `default=2`. +- `test_lib_sh_passes_max_cols_and_rows` — content-asserts lib.sh contains the `--max-cols "${MAM_MAX_PANE_COLS:-2}"` / `--max-rows "${MAM_MAX_PANE_ROWS:-2}"` wiring (regression guard). PASSED. + +### W5 — Tests for deterministic 2×2 trajectory ✅ +Trajectory (both GUI `test_layout_single_workspace_54x23_compact_tiling_tier1` and headless `test_headless_0x0_transitions` / `test_headless_max_columns_growth_guard`): + +| N | direction | target | reason | +|---|---|---|---| +| 1 | `right` | p1 | `new_column_right` | +| 2 | `down` | p1 | `fill_column` | +| 3 | `down` | p2 | `fill_column` | +| 4 | `overflow` | — | `grid_capacity_reached` | + +`test_headless_max_columns_growth_guard` additionally asserts the default cap holds when the caller **omits** `max_columns` — directly pinning the W4 no-`None` fix. + +--- + +## 3. Lint + +- `python -m py_compile .agents/skills/lib_py/layout.py` → **OK**. +- `bash -n .agents/skills/lib.sh` → **OK**. +- No `ruff`/`flake8`/`pylint` config in the repo, so Python "lint" = `py_compile` + manual review: helpers are single-responsibility, typed, and documented; `_env_int`'s docstring explains the skip-invalid (don't-abort-on-typo) design, consistent with lib.sh's silent-fallback safety model. No dead code; no unreachable branches observed. + +## 4. Operability + +- **Behavior change (N=1 → right)** is the intended Rev.2 contract; all callers/tests updated, no external caller breaks (lib.sh only consumes `direction`/`target`). +- **2×2 cap** means overflow at 4 panes; `.mam.env.example` documents the cap and warns against raising `MAM_MAX_PANE_ROWS` pre-resize-normalisation. Operators needing more set the env. +- **Defensive `_env_int`** skips unparsable values rather than raising — an operator typo cannot take the layout call down (lib.sh would still fall back to `right`), matching the codebase philosophy. + +## 5. Loss / Omission Check + +None for W1–W5. All four W4 layers present (signature + guard + argparse default + lib.sh + env doc). The only gap is **documentation**, not implementation: + +- **F1 (traceability)**: `tests/test_b19_headless_reconcile_fixes.py` was modified but was **not enumerated in the brief's changeset list**. The change is correct and consistent with W2 (N=1 → right; 80-wide < 2×60 min-cols → overflow; 160-wide → right). Flagging only because the brief under-described the diff. + +## 6. Findings (non-blocking) + +- **F1 — Unlisted modified file**: `tests/test_b19_headless_reconcile_fixes.py` (see §5). Traceability/coverage observation only; the change itself is sound. +- **F2 — Scope bundling**: the `grok` resume-allowlist edits (`resolve_session_id.sh`/`resume_session.sh`) remain bundled with the layout deliverable in the working tree. They belong to agent-onboarding; bundling muddies attribution. Already reviewed/PASSED in `a9de9b22`; unchanged here. Observation only. +- **F3 — Long lib.sh line (~220 chars at :435)**: a backslash continuation would aid readability, but it matches the pre-existing one-liner style and `bash -n` passes. Cosmetic. +- **F4 — Magic tolerances**: `_full_height_pane(tol=2)` and `_group_columns(abs(…)≤2)` share an implicit 2-cell rounding tolerance that isn't a named constant. Reasonable and internally consistent; minor. +- **F5 — Resize-normalisation warning**: `.mam.env.example`'s "do not raise … until a resize-normalisation pass exists" is good guidance but references no tracking issue. Minor. + +## 7. Verdict + +The implementation faithfully realizes the approved Rev.2 plan across all five work items. The plan's central risk — the production-inert `max_columns` ("double omission") — is closed via triple-layered defaults and is pinned by integration tests at the real CLI entrypoint (flag path, env path, and default path) plus a lib.sh content guard. The single decision table is clean, the 2×2 trajectory is deterministic in both GUI and headless, and all tests pass (`test_layout.py` 38, `test_tier1_unit.py` 61, `test_b19` 6; `py_compile`/`bash -n` clean). Findings are traceability/scope/cosmetic and non-blocking. No redesign-level rework is required. + +[VERDICT: PASS] \ No newline at end of file diff --git a/.agents/reports/reviewer-cline-01/report-a9de9b22.md b/.agents/reports/reviewer-cline-01/report-a9de9b22.md new file mode 100644 index 0000000..c30b986 --- /dev/null +++ b/.agents/reports/reviewer-cline-01/report-a9de9b22.md @@ -0,0 +1,66 @@ +# Code Review Report — Job `a9de9b22` + +- **Reviewer**: cline (session: `herdr:reviewer-cline-01`) +- **Subject**: Layout engine improvement plan (Rev.2) + cumulative `git diff` +- **Changeset**: 2 modified shell scripts + 1 new report (untracked) + - `M .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh` + - `M .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh` + - `?? .agents/reports/layout_engine_improvement_plan.md` (387 lines) +- **Date**: 2026-08-26 + +--- + +## 1. Scope + +The brief's stated work goal is the **layout engine improvement plan** (analyze Herdr skew, produce `.agents/reports/layout_engine_improvement_plan.md`). The cumulative `git diff` is a **mixed changeset**: the plan report (the deliverable) **plus** two `grok` agent-allowlist additions in the resume skill — a separate concern. Both are reviewed below per the brief's "누적 변경분(git diff)" instruction. + +Note: the plan is a **forward-looking spec**; it does **not** modify `layout.py`/`lib.sh` in this changeset (those are future W1–W8 tasks). The only **runtime** behavior change in this diff is the `grok` allowlist. + +## 2. Layout Plan — Verification Against Actual Code + +The plan's high-stakes technical claims were checked against the live source: + +| Claim | Source location | Verified | +|---|---|---| +| Headless uses `n % 2` parity | `layout.py:115` `if n % 2 == 1:` → `headless_odd_down` (:120); even → `headless_even_right` (:125) | ✅ | +| `lib.sh` does **not** pass `--max-cols` | `lib.sh:435` pipes `--min-cols … --min-rows … --sample-pane …` only; no `--max-cols` | ✅ | +| `--max-cols` default returns `None` when env unset | `layout.py:203` `default=_env_int("MAM_MAX_COLS","MAM_MAX_PANE_COLS")` — no `default=` arg → `_env_int` returns `None` | ✅ | +| "Double omission" makes signature-only fix inert in production | env unset + no flag ⇒ `max_columns=None` ⇒ no cap, regardless of signature default | ✅ Accurate | +| `.mam.env.example` self-inconsistent | `:147` `#default: (unset -> no column cap)` vs `:148` `# MAM_MAX_PANE_COLS=3`; no `MAM_MAX_PANE_ROWS` anywhere | ✅ | +| Existing tests fix the old contract | `test_layout.py:145` N=1→down; `:400` n=2→right; `:414` n=4 unlimited→right | ✅ | +| `fill_singleton_column` checks `len(col)==1` | `layout.py:150` `if len(col) == 1:` → `fill_singleton_column` (:159) | ✅ | + +**Assessment**: The plan's central alarm — that wiring `max_columns` only via the signature default would pass tests but be **silently inert in production** (because `lib.sh` never passes `--max-cols` and `_env_int` yields `None`) — is **technically correct** and is the most valuable finding in the document. The proposed remedy (`lib.sh:435` explicitly pass `--max-cols`/`--max-rows` with `:-2` shell defaults + add `MAM_MAX_PANE_ROWS=2`) is the right fix. The single-decision-table design (§2), the trajectory correction (§3, n=4 stops at 2×2), and the parity-rejection rationale (§1) are internally consistent and actionable (Creator sign-off §13). Three objections sustained + three self-corrections (C-3/C-4/C-5) is a sound revision record. + +As a **report** deliverable, "lint" is N/A; **operability** (actionable/correct) ✅; **loss** — minor, see §5. + +## 3. `grok` Allowlist Changes — Verification + +- `resolve_session_id.sh:39` adds `grok` to the `case`; error message `:40` updated to list grok. ✅ +- `resume_session.sh:45` adds `grok` to the `case` (error `:46` is generic). ✅ +- **Coherence**: `resume_session.sh:112` **already** had a `grok)` fallback (`--resume $UUID --permission-mode bypassPermissions`) before this diff — but the top-level validation `:45` rejected `grok`, so that path was **dead/unreachable**. This diff closes the gap: validation now matches the pre-existing downstream support. `grok` is a registered first-class adapter (`lib_py/agents/registry.py:16` `GrokAgentAdapter`), so resolution/resume are grok-aware end-to-end. +- **Consistency**: brings the resume skill in line with peer skills (`create_session.sh:93`, `stop_session.sh:97`, `orc_onboard.sh:107` already accept grok). The resume skill was the last holdout. +- `bash -n`: both scripts pass. +## 4. Test Results + +| Suite | Result | +|---|---| +| `bash -n` resolve_session_id.sh / resume_session.sh | OK | +| `pytest tests/test_layout.py -q` | **26 passed** (unaffected — diff doesn't touch `layout.py`) | +| `pytest tests/test_tier1_unit.py -q -k 'resume or grok or agent or find_workspace'` | **13 passed**, 48 deselected | +| `pytest tests/test_tier2_component.py -q -k 'resume'` | **8 passed**, 32 deselected | + +No regressions from the `grok` additions; no test asserts the old grok-rejecting behavior. + +## 5. Findings (non-blocking) + +- **F1 — Mixed/unrelated changeset (scope hygiene)**: the `grok` allowlist changes belong to the agent-onboarding concern, not the layout-engine task. Bundling them with the plan report muddies attribution. Observation only (the brief explicitly includes the cumulative diff, so both are reviewed). +- **F2 — Stale usage docstrings (cosmetic)**: `resolve_session_id.sh:4,16` and `resume_session.sh:12` still advertise `--agent ` without `grok`, while the `case` now accepts grok. `--help` understates accepted agents. Pre-existing repo-wide pattern (create/stop share it), but the diff touched these files and could have aligned the docstrings in the same touch. Non-blocking. +- **F3 — Minor clarity in plan §1**: the §1 table's "홀짝 반전 결과" column models the *hypothetical literal-reversal* implementation (n=1→right…), whereas the actual current code is the *non-reversed* parity (`odd→down / even→right`). Both are parity-based and both diverge from Rev.2's right-first trajectory, so the thesis holds; §1 lines 45–52 correctly state the *real* current test assertions. A reader skimming only the table could momentarily mis-map it to the current code. Cosmetic. +- **F4 — Plan is spec-only this pass**: no `layout.py`/`lib.sh` mutation occurred in this changeset, so the "implementation" reviewed here is a plan + a small agent-allowlist fix — not the layout rework itself. Worth stating to set expectations for the next (W1–W8) implementation pass. + +## 6. Verdict + +The layout plan is technically sound and its pivotal claim (production-inert `max_columns` wiring) is verified against live code; the `grok` allowlist changes are correct, coherent, and tested green. Findings are cosmetic/scope-only and non-blocking. No redesign-level rework is required. + +[VERDICT: PASS]