2 Commits
17 changed files with 541 additions and 27 deletions
@@ -0,0 +1,169 @@
# 🔍 Cross Code Review — Job ddc8d9f1
- **Reviewer**: `planner-reviewer-claude-01` (role: `planner,reviewer`)
- **Target**: 2xK grid layout engine — `MAM_MIN_PANE_COLS` 기본값 60 → 40 및 단일 워크스페이스 다중 페인 타일링
- **Reviewed files**: `.agents/skills/lib.sh`, `.agents/skills/lib_py/layout.py`, `.mam.env.example`, `tests/test_layout.py`, `tests/test_tier1_unit.py`, `tests/test_a4_adapter_contract.py`
---
## ⚠️ 검증 범위 고지 (Verification Scope Disclaimer)
**본 세션에서 `pytest` 스위트를 실행하지 못했습니다.** 실행 시도가 중단되어(사용자 거부) 브리프 4번 요구사항 *"Ensure full pytest test suite passes"***실측으로 확인되지 않았습니다.**
따라서 아래 판정은 다음 범위로 한정됩니다:
- ✅ 소스 정적 분석 (`layout.py` 전체 로직 판독)
- ✅ 신규 테스트의 **모든 단언을 엔진 분기에 대입한 수동 트레이스**
-`grep` 기반 상수 드리프트 전수 조사
-**테스트 실행 결과 (미수행)**
수치·분기 추적은 결정론적 정수 연산이라 수동 검증의 신뢰도가 높지만, 실행 확인은 별도로 이루어져야 합니다. §5에 잔여 항목을 명시했습니다.
---
## 1. 변경 요약
| 위치 | 변경 | 판정 |
|---|---|---|
| `lib_py/layout.py:73` | `compute_2xk_layout(min_cols=60)``40` | ✅ |
| `lib_py/layout.py:201` | `--min-cols` 기본값 `_env_int(..., default=60)``40` | ✅ |
| `lib_py/layout.py:179` | 독스트링 `60 default``40 default` | ✅ |
| `lib.sh:432` | `${MAM_MIN_PANE_COLS:-60}``:-40` | ✅ |
| `.mam.env.example:132-133` | 주석 `#default: 60` 및 예시 `=60``40` | ✅ |
| `tests/test_layout.py:290` | lib.sh 소스 스니펫 가드 문자열 동기화 | ✅ |
**3중 기본값 동기화 확인**: 이 코드베이스는 동일한 기본값을 **세 곳**(shell 파라미터 확장, Python 시그니처, Python argparse)에 중복 보유합니다. 세 곳 모두 40으로 일치하며 `.mam.env.example` 문서값까지 4중 일치합니다. 드리프트 없음.
> **참고**: `lib.sh:432` 는 항상 `--min-cols` 를 **명시 전달**하므로 실운영 경로에서 `layout.py:201` 의 argparse 기본값은 도달하지 않습니다. 201번 줄은 CLI 직접 호출·테스트 경로용 fallback 입니다. 두 값이 어긋나도 즉시 드러나지 않는 구조이므로 §4에 가드 제안을 남깁니다.
---
## 2. 로직 정합성 — 신규 테스트 수동 트레이스
`compute_2xk_layout` 의 분기를 신규 단언에 그대로 대입해 전건 검증했습니다. 폭 판정은 `layout.py:169``width // 2 < min_cols` 단일 게이트입니다.
### 2.1 경계값 (80 / 79 cols)
| 입력 | 계산 | 도달 분기 | 기대 | 실제 |
|---|---|---|---|---|
| 2페인 × w=80 (x=0 동일열) | `80 // 2 = 40`, `40 < 40` = False | `:172 new_column_right` | `right`, not overflow | ✅ 일치 |
| 2페인 × w=79 | `79 // 2 = 39`, `39 < 40` = True | `:170 column_width_overflow` | `overflow` | ✅ 일치 |
**80이 정확한 하한**임이 확인됩니다(`>= 80` 에서 분할 가능). 브리프 2번 요구사항 *"width >= 80 에서 조기 overflow 금지"* 는 상수 변경만으로 산술적으로 충족되며, 별도 분기 추가가 불필요합니다 — **엔진 로직 무변경은 올바른 판단**입니다. 불필요한 특수 케이스를 넣지 않은 점을 긍정 평가합니다.
### 2.2 90 / 100 col 단일 워크스페이스 타일링 (1→2→3→4→overflow)
`total_w ∈ {90, 100}`, `half_w = total_w // 2 ∈ {45, 50}` 기준 전 단계 추적:
| 단계 | 입력 형상 | 판정 경로 | 결과 |
|---|---|---|---|
| 1→2 | 1페인 `w×40` | `:94` `40//2 = 20 >= min_rows 20` → False(제약 아님) → `:102` | `down` / `single_pane_split_down`, target `p1` ✅ |
| 2→3 | 2페인 x=0 단일열 | singleton 없음 → `:169` `45//2=22`? **아니오** — 이 시점 페인 폭은 아직 `total_w`(90/100) → `90//2=45 >= 40` | `right` / `new_column_right`, target `p1`(`columns[-1][0]`) ✅ |
| 3→4 | `[p1,p2]` @x=0, `[p3]` @x=half_w | `:148-154` singleton 열 `[p3]` 탐지 → 높이 `40//2=20 >= 20` | `down` / `fill_singleton_column`, target `p3` ✅ |
| 4→5 | 2열 × 2페인 완성 | singleton 없음, `max_columns=None``:169` `45//2=22 < 40` (100col: `50//2=25 < 40`) | `overflow` / `column_width_overflow` ✅ |
**핵심 확인 사항 2건**:
1. **1→2 단계의 높이 경계**: `height=40` 에서 `40 // 2 = 20`, `min_rows=20`**같음**. `:94` 조건은 `< min_rows` 이므로 False → 정상적으로 `down` 진입. `<=` 였다면 오분기했을 지점으로, 테스트가 이 경계를 정확히 짚고 있습니다.
2. **4페인에서의 의도적 overflow**: `min_cols=40` 에서 90~100col 워크스페이스는 **최대 4에이전트**가 상한이며 5번째는 새 워크스페이스로 넘어갑니다. 브리프 목표(*"3-4 agents in ~100-col terminal"*)와 정확히 부합하고, 테스트가 이 상한을 명시적으로 고정하고 있어 향후 회귀 시 즉시 검출됩니다. ✅
### 2.3 컬럼 그룹핑 정합성
`:129-139` 의 x좌표 퍼지 그룹핑(임계 2col)에 신규 픽스처 대입 시:
- 90col: x ∈ {0, 45} → `|0-45| = 45 > 2` → 2개 열로 정확히 분리 ✅
- 100col: x ∈ {0, 50} → 동일 ✅
퍼지 임계값 2와 충돌하는 좌표가 없어 그룹핑 오분류 위험이 없습니다.
---
## 3. 회귀 영향 분석 (유실 관점)
기본값 변경은 **기본값에 의존하는 기존 테스트**에만 파급됩니다. 전수 조사 결과:
| 기존 테스트 | 기본값 의존 여부 | 영향 |
|---|---|---|
| `test_layout.py:28~262` (8건) | `min_cols=60` **명시 전달** | 영향 없음 ✅ |
| `test_layout.py:137,191` | `min_cols=30` 명시 | 영향 없음 ✅ |
| `test_layout.py:207` (subprocess) | `--min-cols 60` 명시 | 영향 없음 ✅ |
| `test_layout.py:358,374` | `--min-cols 30` 명시 | 영향 없음 ✅ |
| `test_j1_env_zero_min_cols_matches_flag_zero` | `_ZERO_TRAP` 기본값 실행 포함 | **영향 검토 필요 → 아래** |
| `test_j1b_invalid_alias_does_not_shadow...` | 기본값 실행 비교 | 동일 ✅ |
**J-1 계열 정밀 검토** (`test_layout.py:432` 주석 기준 `_ZERO_TRAP` = 단일 페인 `50×30`):
- `height // 2 = 15 < min_rows 20``:94` 제약 분기 진입
- `width // 2 = 25``min_cols` 와 비교: 기존 `25 < 60` → overflow / 신규 `25 < 40`**overflow (동일)**
- 즉 기본값이 60이든 40이든 `_ZERO_TRAP` 의 결과는 `single_pane_overflow` 로 불변. **J-1/C-2 불변식 보존 확인**
또한 J-1b는 "기본값 실행 == 기본값 실행" 형태의 자기참조 비교라 기본값 자체와 무관하게 성립합니다.
`test_layout.py:290` 의 lib.sh 소스 스니펫 가드는 **문자열 완전 일치** 검사이므로 `lib.sh:432` 와 함께 갱신되지 않았다면 즉시 실패했을 항목입니다. 양쪽 모두 `:-40` 으로 동기화되어 있음을 대조 확인했습니다 ✅
---
## 4. 지적 사항 (모두 비차단 / Non-blocking)
차단 결함(P0/P1)은 발견되지 않았습니다. 아래는 개선 권고입니다.
### 🟡 N-1 (P3) — `test_herdr_shim_contract.py:100` 의 `MAM_MIN_PANE_COLS=60` 미검토
`tests/test_herdr_shim_contract.py:92,100` 의 H-13 케이스가 `export MAM_MIN_PANE_COLS=60` 을 사용합니다. 이는 **환경변수 오버라이드 동작 자체**를 검증하는 케이스이므로 기본값 변경과 논리적으로 독립이며(명시 오버라이드 경로), 정상 통과가 예상됩니다. 다만 파일 본문을 열람하지 못해 **단언 내용까지는 확인하지 못했습니다.**
**개선 방향**: 이 테스트가 "60이 아닌 값이 적용됨"을 검증하는 의도라면, 이제 기본값 40과 오버라이드 값 60이 명확히 구분되어 오히려 대조가 선명해집니다. 확인만 권고합니다.
### 🟡 N-2 (P3) — `IMPROVEMENTS.md:49` 의 `min_cols=60` 잔존
```
IMPROVEMENTS.md:49: ... 해상도 오버플로 가드(`min_cols=60`, `min_rows=20`) ...
```
해당 줄은 **엔진 최초 도입 시점을 기록한 변경 이력**이므로 당시 값 60을 남기는 것이 이력 문서로서는 정확합니다. 다만 현재 이 저장소에서 **60을 기본값이라 서술하는 유일한 문서**가 되었습니다.
**개선 방향 (택1)**: (a) 그대로 두되 이번 변경을 `IMPROVEMENTS.md` 신규 항목으로 추가하여 60→40 전환 이력을 잇는다 — **권장**. (b) 해당 줄에 `(현행 40, 잡 ddc8d9f1에서 변경)` 각주를 붙인다. 이력 문서를 소급 수정하는 방식은 권장하지 않습니다.
### 🟡 N-3 (P3) — 기본값 4중 중복에 대한 파리티 가드 부재
동일 상수가 `lib.sh:432` / `layout.py:73` / `layout.py:201` / `.mam.env.example:133` 4곳에 문자열로 중복 존재합니다. `test_layout.py:290` 이 lib.sh↔테스트 스니펫 쌍만 고정할 뿐, **`layout.py:73` 시그니처 기본값과 `layout.py:201` argparse 기본값의 일치는 어떤 테스트도 강제하지 않습니다.** 두 값이 어긋나면 CLI 경로와 라이브러리 임포트 경로가 조용히 갈라집니다.
**개선 방향 (구체안)**:
```python
# tests/test_layout.py
import inspect
from lib_py.layout import compute_2xk_layout
def test_default_min_cols_parity_across_entrypoints():
"""시그니처 기본값 == argparse 기본값 == lib.sh fallback."""
sig_default = inspect.signature(compute_2xk_layout).parameters["min_cols"].default
assert sig_default == 40
# argparse 경로: env 미설정 시 동일 결정을 내야 함
assert _run_layout(_ZERO_TRAP) == _run_layout(_ZERO_TRAP, ("--min-cols", str(sig_default)))
# lib.sh fallback 문자열
lib_sh = (REPO_ROOT / ".agents/skills/lib.sh").read_text()
assert f'${{MAM_MIN_PANE_COLS:-{sig_default}}}' in lib_sh
```
이는 이전 잡에서 `ready_tokens``lib.sh`/`claude.py` 양쪽에 중복된 것과 **동일 유형의 구조적 취약점**이며, 같은 처방이 적용됩니다. 별도 잡으로 분리해도 무방합니다.
### 🟢 N-4 (P4) — 워킹트리 위생
`git status``m nats-docker` (서브모듈 dirty, `5db38da...-dirty`) 가 포함되어 있습니다. 본 변경과 무관한 오염이며 커밋 전 정리를 권고합니다. 또한 `tests/test_layout.py` 말미에 빈 줄 3개(`+++`)가 추가되어 있어 PEP8 관점의 사소한 정리 여지가 있습니다. 기능 영향 없음.
### ️ N-5 (정보) — 누적 diff 내 `test_a4_adapter_contract.py` 변경
`ready_tokens``Claude Code|Opus|Sonnet|Haiku` 를 추가한 직전 잡의 변경분이 누적 diff에 포함되어 있습니다. 계약 테스트의 기대값이 `lib.sh` / `claude.py` 양쪽 구현과 3자 일치함을 대조 확인했습니다 ✅ (본 잡 범위 외)
---
## 5. 잔여 검증 항목 (Outstanding)
| # | 항목 | 상태 |
|---|---|---|
| V-1 | `pytest tests/ -q` 전체 통과 | ❌ **미수행** — 본 세션에서 실행 중단됨 |
| V-2 | `test_herdr_shim_contract.py` H-13 단언 내용 | ⚠️ 미열람 (영향 없음으로 추정, N-1) |
**V-1은 머지 전 반드시 실측되어야 합니다.** 정적 분석상 실패를 유발할 요인은 발견하지 못했으나(§3 회귀 영향 전무), 이는 예측이지 관측이 아닙니다.
---
## 6. 총평
변경은 **상수 1개의 값 조정과 그에 대한 4중 동기화**라는 최소 표면적을 정확히 지켰습니다. 엔진 분기 로직을 건드리지 않고 브리프의 4개 요구사항을 충족한 점, 특히 요구사항 2를 위해 불필요한 특수 분기를 추가하지 않고 산술로 해소한 점이 설계적으로 건전합니다.
신규 테스트는 단순 happy-path에 머물지 않고 **80/79 경계**, **height 40//2 == min_rows 20 동등 경계**, **4페인 상한 후 overflow** 라는 세 개의 실질적 경계를 고정합니다. 기존 J-1/C-2 불변식도 보존됩니다.
지적 사항 4건은 모두 P3 이하이며 문서 이력·테스트 위생·워킹트리 정리 범주로, 어느 것도 현재 동작을 해치거나 결함을 은폐하지 않습니다. 설계 변경이나 재계획이 필요한 사안은 없습니다.
**단, 본 PASS는 §5 V-1(전체 테스트 실행) 이 별도로 확인된다는 전제 위에 성립합니다.** 정적 검토 범위에서는 차단 사유가 없습니다.
[VERDICT: PASS]
@@ -0,0 +1,103 @@
# Review Report — Job bb360685
- **Reviewer**: cline (herdr session `reviewer-cline-01`, role: reviewer)
- **Job ID**: bb360685
- **Reviewed branch**: `refactor` (changes unstaged in working tree)
- **Scope**: Cross code review (lint / operability / drift) of the diff for
"Improve 2xK grid layout engine and prevent premature workspace overflow".
- **Diff stat**: 7 files, +236 / -9 (plus a dirty submodule).
## 1. Change Inventory
| File | Change | Category |
|------|--------|----------|
| `.agents/skills/lib_py/layout.py` | `compute_2xk_layout` default `min_cols` 60→40; CLI `--min-cols` default 60→40; `_env_int` docstring 60→40 | Core logic (task goal #1, #2) |
| `.agents/skills/lib.sh:432` | Fallback `${MAM_MIN_PANE_COLS:-60}``:-40` | Core logic (task goal #1) |
| `.mam.env.example` | Documented default `MAM_MIN_PANE_COLS` 60→40 | Config/docs (task goal #1) |
| `tests/test_layout.py` | Updated lib.sh snippet expectation (`:-60``:-40`); J-1 docstring 60→40; +4 new tests (default-40, 80-col boundary, 90/100-col tiling) | Tests (task goal #3) |
| `tests/test_tier1_unit.py` | +2 new Tier-1 tests (default-40, 90/100-col tiling) | Tests (task goal #3) |
| `tests/test_a4_adapter_contract.py` | Widened `claude` `ready_tokens` regex (`+|Claude Code|Opus|Sonnet|Haiku`) | **Unrelated to layout task** |
| `nats-docker` (submodule) | `PRIVATE_SERVER.md` modified → submodule marked `-dirty` | **Stray / drift, unrelated** |
## 2. Lint / Syntax
- `python -m py_compile lib_py/layout.py`**OK**
- `bash -n .agents/skills/lib.sh`**OK**
- No leftover `MAM_MIN_PANE_COLS:-60` fallbacks anywhere in `.sh`/`.py`. The only
remaining `60` references are *explicit* `min_cols=60` arguments in pre-existing
layout tests (legitimate — they exercise the 60 configuration, not the default)
and one contrast docstring line. The default is consistently 40 across all three
authoritative sites (function signature, CLI argparse, lib.sh fallback) and the
env example. **No orphans.**
## 3. Operability — Goal-by-Goal Verification
### Goal #1 — Default 60→40 to enable 3-4 agents in ~100-col windows
- Verified all three default sites are 40 and consistent.
- Practical effect: with `min_cols=60`, a 100-col pane split right yields 50-col
halves → `50 < 60` → immediate `column_width_overflow` (could not even open a
2nd column). With `min_cols=40`, `50 >= 40` → 2 columns (4 panes) fit before
overflow. The change materially enables 3-4 agents per ~100-col workspace, not
merely cosmetic. ✓
### Goal #2 — Clean 2-column split at width >= 80 without premature overflow
- Boundary predicate is `rightmost_top_pane.width // 2 < min_cols` (strict `<`).
At width 80: `80//2 = 40`, `40 < 40` is **False** → splits right (not overflow).
At width 79: `79//2 = 39`, `39 < 40` is **True**`column_width_overflow`.
- CLI end-to-end confirmation (mirrors the `lib.sh` invocation path):
- 80-col payload → `right p1`
- 79-col payload → `overflow p1`
- The boundary is exactly at 80 and behaves as specified. ✓
### Goal #3 — Updated unit tests assert default 40 & 90-100 col tiling
- New tests present in both suites:
- `test_default_min_cols_is_40` / `test_layout_default_min_cols_40_in_tier1`
- `test_80_col_2_column_splitting_boundary`
- `test_90_col_single_workspace_multi_pane_tiling` /
`test_100_col_single_workspace_multi_pane_tiling` /
`test_layout_single_workspace_90_100_cols_tiling_tier1`
- Tiling tests verify the full 1→2→3→4→(5th overflow) progression with correct
target panes and `column_width_overflow` reason. Traced the column-grouping
logic: singleton-column fill at step 3→4 and width-constrained overflow at
step 4→5 are both reached correctly. ✓
### Goal #4 — Full pytest suite passes
- Targeted run of the three affected unit-test files:
`tests/test_layout.py tests/test_tier1_unit.py tests/test_a4_adapter_contract.py`
**99 passed in 10.97s**. ✓
- The complete `pytest tests/` suite could not be fully executed within this
review's time budget (integration tests are long-running), but every file
touched by the diff passes, and no unit-test regression is introduced.
## 4. Findings (advisory, non-blocking)
### F-1 (Hygiene/Scope): `test_a4_adapter_contract.py` change is out of scope
- The `claude` `ready_tokens` regex widening (`+|Claude Code|Opus|Sonnet|Haiku`)
is a correct, additive adapter fix and the contract test passes — but it is
**unrelated** to the 2xK layout-engine task. Bundling it into this diff blurs
traceability.
- **Direction**: Split into its own commit (`fix(adapter): broaden claude
ready_tokens`) before merging. No code change required for the layout work.
### F-2 (Drift): `nats-docker` submodule is dirty
- `git diff nats-docker` shows the submodule pointer unchanged but flagged
`-dirty`; `git -C nats-docker status` shows ` M PRIVATE_SERVER.md`.
- This is a stray local modification inside the submodule, unrelated to the
task, and risks being accidentally staged/committed alongside the layout
changes.
- **Direction**: Revert the stray edit (`git -C nats-docker checkout --
PRIVATE_SERVER.md`) or leave the submodule unstaged. Do not commit the
submodule pointer change with this work.
## 5. Summary
The core implementation correctly and consistently lowers the 2xK layout
`min_cols` default from 60 to 40 across `lib.sh`, `layout.py` (function +
CLI + docstring), and `.mam.env.example`, with matching, passing unit tests
covering the 80-col boundary and 90/100-col single-workspace tiling. Syntax
and CLI operability are verified. The two findings (F-1 out-of-scope adapter
regex, F-2 dirty submodule) are hygiene/drift items that do not affect the
layout engine's correctness or operability and require only commit
housekeeping, not a redesign.
[VERDICT: PASS]
+1 -1
View File
@@ -429,7 +429,7 @@ except Exception:
split_dir=""
if [ -n "$sample_pane" ]; then
layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "")
read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-60}" --min-rows "${MAM_MIN_PANE_ROWS:-20}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-40}" --min-rows "${MAM_MIN_PANE_ROWS:-20}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
split_dir="${split_dir:-right}"
sample_pane="${split_target:-$sample_pane}"
fi
+3 -3
View File
@@ -70,7 +70,7 @@ def extract_panes(data: Dict[str, Any]) -> List[PaneInfo]:
def compute_2xk_layout(
data: Dict[str, Any],
min_cols: int = 60,
min_cols: int = 40,
min_rows: int = 20,
max_columns: Optional[int] = None,
default_anchor_id: Optional[str] = None
@@ -176,7 +176,7 @@ 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).
legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 40 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
@@ -198,7 +198,7 @@ def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]:
def main():
parser = argparse.ArgumentParser(description="Compute 2xK grid TUI layout split direction")
parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=60))
parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=40))
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"))
parser.add_argument("--sample-pane", type=str, default=None)
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-create
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-delegate-job
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, cline, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos, windows]
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-monitor
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-orc-onboard
description: Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture.
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-resume
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-status
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: multi-agent-mux-stop
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
version: 2.2.0
version: 2.2.1
author: godopu
license: MIT
platforms: [linux, macos]
+2 -2
View File
@@ -129,8 +129,8 @@
# SKS_EMPTY_GIVEUP=3
# Minimum columns a pane must retain after a vertical split (2xK layout engine).
#default: 60
# MAM_MIN_PANE_COLS=60
#default: 40
# MAM_MIN_PANE_COLS=40
# Minimum rows a pane must retain after a horizontal split (2xK layout engine).
#default: 20
+25 -10
View File
@@ -6,10 +6,11 @@
## 📌 현재 버전 개요 (Current Release)
- **프레임워크 버전**: `v2.2.0`
- **프레임워크 버전**: `v2.2.1`
- **최신 릴리스 일시**: 2026-08-24 (KST)
- **기준 브랜치**: `main`
- **핵심 아키텍처**:
- **Single-Workspace 2xK Multi-Pane Tiling Optimization**: 기본 최소 페인 너비 완화(`MAM_MIN_PANE_COLS=40`)로 80~100컬럼 창에서 3~4개 에이전트 단일 워크스페이스 타일링 보장
- **`--herdr-workspace` Option & Runtime Label Sync**: Herdr 세션 내 워크스페이스 라벨 독립 지정 및 런타임/YAML 실시간 동기화
- **Legacy Fallback Chain Decoupling**: 데몬 소켓(`herdr_session`)과 워크스페이스 라벨(`herdr_workspace`) 조회 체인 원천 분리
- **Modern Agent Adapter & TUI Readiness**: 최신 Claude Code(`v2.1.241`) 배너 및 4대 에이전트 TUI 초고속 감지
@@ -21,23 +22,37 @@
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v2.2.0`으로 동기화되어 배포됩니다.
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v2.2.1`으로 동기화되어 배포됩니다.
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
| :--- | :---: | :--- | :---: |
| **`multi-agent-mux-create`** | `2.2.0` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `2.2.0` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `2.2.0` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `2.2.0` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `2.2.0` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `2.2.0` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `2.2.0` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `2.2.0` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
| **`multi-agent-mux-create`** | `2.2.1` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `2.2.1` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `2.2.1` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `2.2.1` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `2.2.1` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `2.2.1` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `2.2.1` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `2.2.1` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
---
## 📋 버전별 상세 변경 내역 (Changelog)
### 🚀 `v2.2.1` — Single-Workspace 2xK Multi-Pane Tiling Optimization & Premature Overflow Fix (2026-08-24)
> **주요 마일스톤**: `MAM_MIN_PANE_COLS` 기본값 60→40 완화, 표준 80~100컬럼 터미널 뷰포트에서 조기 워크스페이스 오버플로(가상 데스크톱 분리) 방지 및 단일 워크스페이스 2x2 통합 타일링 완성, 신규 80/79 경계 및 90/100 col 타일링 테스트 6종 추가, 다중 에이전트 피어 리뷰 100% PASS 달성.
#### 1. 2xK 레이아웃 엔진 최소 폭 완화 (`lib_py/layout.py`, `lib.sh`)
- `compute_2xk_layout` 기본 `min_cols` 및 CLI `--min-cols`, `lib.sh:432``${MAM_MIN_PANE_COLS:-40}`, `.mam.env.example` 문서를 `40`으로 4중 일치화.
- 90~100컬럼 너비 터미널에서 3번째, 4번째 에이전트 생성 시 불필요하게 가상 데스크톱(Workspace)이 분리되던 현상 완전 해소.
#### 2. 경계값 및 타일링 자동화 테스트 확충 (`tests/test_layout.py`, `tests/test_tier1_unit.py`)
- `80` 컬럼(분할 성공) vs `79` 컬럼(오버플로) 하한 경계값 검증.
- `90``100` 컬럼 단일 워크스페이스 1→2→3→4 단계 2x2 타일링 및 5번째 에이전트 오버플로 전 과정 수명 주기 검증.
---
### 🚀 `v2.2.0` — Herdr Workspace Label Standardization, Runtime Sync & Legacy Fallback Decoupling (2026-08-24)
> **주요 마일스톤**: `--herdr-workspace` 옵션 전 스킬 도입 및 YAML 독립 직렬화, Herdr 런타임 워크스페이스 레이블 실시간 동기화, 레거시 소켓 폴백 체인 분리(Breaking Change 방어), 최신 Claude Code TUI 감지 토큰 반영, 27개 신규 테스트 추가 및 만장일치 PASS 달성.
+1 -1
View File
@@ -63,7 +63,7 @@ def test_adapter_required_properties():
getattr(base, prop)
expected = {
'claude': ('Anthropic|Assistant|Chat|Welcome', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
'claude': ('Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
+143 -2
View File
@@ -287,7 +287,7 @@ split_dir=""
# Exact snippet from lib.sh:429-435
if [ -n "$sample_pane" ]; then
layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "")
read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-60}}" --min-rows "${{MAM_MIN_PANE_ROWS:-20}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-40}}" --min-rows "${{MAM_MIN_PANE_ROWS:-20}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
split_dir="${{split_dir:-right}}"
sample_pane="${{split_target:-$sample_pane}}"
fi
@@ -435,7 +435,7 @@ _ZERO_TRAP = {"result": {"panes": [
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."""
"""J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 40 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"):
@@ -479,3 +479,144 @@ def test_j1b_invalid_alias_does_not_shadow_the_documented_var():
"MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP)
def test_default_min_cols_is_40():
"""Verify compute_2xk_layout default min_cols is 40.
With width 80 (width//2 = 40):
- min_cols=40 -> 40 >= 40 -> split right (new column).
- min_cols=60 -> 40 < 60 -> overflow.
Default invocation (no min_cols passed) must split right.
"""
payload = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}},
{"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}},
]
}
}
decision = compute_2xk_layout(payload)
assert decision.direction == "right"
assert not decision.is_overflow
assert decision.reason == "new_column_right"
def test_80_col_2_column_splitting_boundary():
"""Verify width >= 80 cols allows 2-column splitting with default min_cols=40,
while width < 80 (e.g. 79) triggers column_width_overflow.
"""
# 80 cols: 80 // 2 = 40 == min_cols(40) -> splits right
payload_80 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}},
{"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}},
]
}
}
d80 = compute_2xk_layout(payload_80)
assert d80.direction == "right"
assert not d80.is_overflow
# 79 cols: 79 // 2 = 39 < min_cols(40) -> overflow
payload_79 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 79, "height": 40}},
{"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 79, "height": 40}},
]
}
}
d79 = compute_2xk_layout(payload_79)
assert d79.direction == "overflow"
assert d79.is_overflow
assert d79.reason == "column_width_overflow"
def test_90_col_single_workspace_multi_pane_tiling():
"""Verify complete 1 -> 2 -> 3 -> 4 pane tiling in a 90-col single workspace.
- 1 pane (90x40): splits down to p1(90x20), p2(90x20)
- 2 panes: splits right to start col 2 -> p3(45x40)
- 3 panes: fills singleton col 2 down -> p4(45x20)
- 4 panes (2x2 grid): 5th agent overflows because 45 // 2 = 22 < 40
"""
# 1 -> 2
p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 90, "height": 40}}]}}
d1 = compute_2xk_layout(p1_layout)
assert d1.direction == "down"
assert d1.target_pane_id == "p1"
# 2 -> 3
p2_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 90, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 90, "height": 20}},
]}}
d2 = compute_2xk_layout(p2_layout)
assert d2.direction == "right"
assert d2.target_pane_id == "p1"
assert not d2.is_overflow
# 3 -> 4
p3_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 45, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 45, "height": 20}},
{"pane_id": "p3", "rect": {"x": 45, "y": 0, "width": 45, "height": 40}},
]}}
d3 = compute_2xk_layout(p3_layout)
assert d3.direction == "down"
assert d3.target_pane_id == "p3"
assert not d3.is_overflow
# 4 -> 5 (overflow to new workspace)
p4_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 45, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 45, "height": 20}},
{"pane_id": "p3", "rect": {"x": 45, "y": 0, "width": 45, "height": 20}},
{"pane_id": "p4", "rect": {"x": 45, "y": 20, "width": 45, "height": 20}},
]}}
d4 = compute_2xk_layout(p4_layout)
assert d4.direction == "overflow"
assert d4.is_overflow
assert d4.reason == "column_width_overflow"
def test_100_col_single_workspace_multi_pane_tiling():
"""Verify complete 1 -> 2 -> 3 -> 4 pane tiling in a 100-col single workspace."""
# 1 -> 2
p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}}]}}
d1 = compute_2xk_layout(p1_layout)
assert d1.direction == "down"
# 2 -> 3
p2_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 100, "height": 20}},
]}}
d2 = compute_2xk_layout(p2_layout)
assert d2.direction == "right"
assert not d2.is_overflow
# 3 -> 4
p3_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 50, "height": 20}},
{"pane_id": "p3", "rect": {"x": 50, "y": 0, "width": 50, "height": 40}},
]}}
d3 = compute_2xk_layout(p3_layout)
assert d3.direction == "down"
assert d3.target_pane_id == "p3"
assert not d3.is_overflow
# 4 -> 5 (overflow to new workspace)
p4_layout = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 50, "height": 20}},
{"pane_id": "p3", "rect": {"x": 50, "y": 0, "width": 50, "height": 20}},
{"pane_id": "p4", "rect": {"x": 50, "y": 20, "width": 50, "height": 20}},
]}}
d4 = compute_2xk_layout(p4_layout)
assert d4.direction == "overflow"
assert d4.is_overflow
assert d4.reason == "column_width_overflow"
+86
View File
@@ -864,5 +864,91 @@ def test_lib_sh_new_session_passes_mam_ws_label(mam_sandbox):
assert '_real_herdr workspace rename "$existing_ws" "$MAM_WS_LABEL"' in content
# ==============================================================================
# FEATURE: 2xK Grid Layout Engine (min_cols=40 & multi-pane workspace tiling)
# ==============================================================================
def test_layout_default_min_cols_40_in_tier1():
"""Verify default min_cols=40 behavior across compute_2xk_layout in Tier 1 suite."""
from lib_py.layout import compute_2xk_layout
# 1. 2 panes in 80 col width (80 // 2 = 40 == min_cols 40) -> splits right cleanly
payload_80 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}},
{"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}}
]
}
}
decision = compute_2xk_layout(payload_80)
assert decision.direction == "right"
assert not decision.is_overflow
assert decision.reason == "new_column_right"
# 2. 2 panes in 79 col width (79 // 2 = 39 < min_cols 40) -> column_width_overflow
payload_79 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 79, "height": 40}},
{"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 79, "height": 40}}
]
}
}
decision_overflow = compute_2xk_layout(payload_79)
assert decision_overflow.direction == "overflow"
assert decision_overflow.is_overflow
assert decision_overflow.reason == "column_width_overflow"
def test_layout_single_workspace_90_100_cols_tiling_tier1():
"""Verify 3-4 agents tiling in standard 90-100 col terminal windows within a single workspace."""
from lib_py.layout import compute_2xk_layout
for total_w in [90, 100]:
half_w = total_w // 2
# Step 1: 1 pane -> 2 panes (split down)
p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": total_w, "height": 40}}]}}
d1 = compute_2xk_layout(p1)
assert d1.direction == "down"
assert d1.target_pane_id == "p1"
assert not d1.is_overflow
# Step 2: 2 panes -> 3 panes (split right to open 2nd column)
p2 = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": total_w, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": total_w, "height": 20}}
]}}
d2 = compute_2xk_layout(p2)
assert d2.direction == "right"
assert d2.target_pane_id == "p1"
assert not d2.is_overflow
# Step 3: 3 panes -> 4 panes (split singleton 2nd column down)
p3 = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": half_w, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": half_w, "height": 20}},
{"pane_id": "p3", "rect": {"x": half_w, "y": 0, "width": half_w, "height": 40}}
]}}
d3 = compute_2xk_layout(p3)
assert d3.direction == "down"
assert d3.target_pane_id == "p3"
assert not d3.is_overflow
# Step 4: 4 panes (2x2 complete) -> 5th agent overflows to fresh workspace
p4 = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": half_w, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": half_w, "height": 20}},
{"pane_id": "p3", "rect": {"x": half_w, "y": 0, "width": half_w, "height": 20}},
{"pane_id": "p4", "rect": {"x": half_w, "y": 20, "width": half_w, "height": 20}}
]}}
d4 = compute_2xk_layout(p4)
assert d4.direction == "overflow"
assert d4.is_overflow
assert d4.reason == "column_width_overflow"