feat(layout): implement deterministic 2xK grid engine with single decision table

- Change N=1->2 split policy from down to right to establish 2 full-height columns
- Share single decision table across GUI and headless modes
- Add _full_height_pane guard to prevent half-height column splits
- Quadruple-wire default max_columns=2 and max_rows=2 across layout.py, lib.sh, and .mam.env.example
- Update unit and integration tests to enforce deterministic 2x2 grid trajectory
This commit is contained in:
2026-08-26 15:10:06 +09:00
parent 5ed9ec79d7
commit 80d2f7f068
6 changed files with 338 additions and 220 deletions
+13 -21
View File
@@ -872,12 +872,10 @@ def test_layout_default_min_cols_15_in_tier1():
"""Verify default min_cols=15 behavior across compute_2xk_layout in Tier 1 suite."""
from lib_py.layout import compute_2xk_layout
# 1. 2 panes in 30 col width (30 // 2 = 15 == min_cols 15) -> splits right cleanly
payload_30 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 30, "height": 20}}
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 40}},
]
}
}
@@ -886,12 +884,10 @@ def test_layout_default_min_cols_15_in_tier1():
assert not decision.is_overflow
assert decision.reason == "new_column_right"
# 2. 2 panes in 29 col width (29 // 2 = 14 < min_cols 15) -> column_width_overflow
payload_29 = {
"result": {
"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 20}},
{"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 29, "height": 20}}
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 40}},
]
}
}
@@ -905,45 +901,41 @@ def test_layout_single_workspace_54x23_compact_tiling_tier1():
"""Verify 3-4 agents tiling in standard 80x24 (54x23 content) terminal windows within a single workspace."""
from lib_py.layout import compute_2xk_layout
# Step 1: 1 pane -> 2 panes (split down without height constraint)
p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}}
d1 = compute_2xk_layout(p1)
assert d1.direction == "down"
assert d1.direction == "right"
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": 26, "y": 1, "width": 54, "height": 11}},
{"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 54, "height": 11}}
{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 23}},
{"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}}
]}}
d2 = compute_2xk_layout(p2)
assert d2.direction == "right"
assert d2.direction == "down"
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": 26, "y": 1, "width": 27, "height": 11}},
{"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}},
{"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}}
{"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}},
{"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}}
]}}
d3 = compute_2xk_layout(p3)
assert d3.direction == "down"
assert d3.target_pane_id == "p3"
assert d3.target_pane_id == "p2"
assert not d3.is_overflow
# Step 4: 4 panes (2x2 complete) -> 5th agent overflows to fresh workspace (27 // 2 = 13 < 15)
p4 = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}},
{"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}},
{"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}},
{"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 11}}
{"pane_id": "p2", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}},
{"pane_id": "p3", "rect": {"x": 26, "y": 12, "width": 27, "height": 12}},
{"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 12}}
]}}
d4 = compute_2xk_layout(p4)
assert d4.direction == "overflow"
assert d4.is_overflow
assert d4.reason == "column_width_overflow"
assert d4.reason == "grid_capacity_reached"
# ==============================================================================