# ๐Ÿ“ ๊ตฌํ˜„ ๊ณ„ํš์„œ Rev.2: ๋ฐฑ๋กœ๊ทธ I-2 / I-3 ์ฒ˜๋ฆฌ (Job `5e4ef463`) - **์ž‘์„ฑ์ผ**: 2026-08-23 - **์—ญํ• **: Planner (`.agents/MULTI_AGENT_RULES.md` ยง1 โ€” Planner ๋Š” ์ €์žฅ์†Œ ์ฝ”๋“œ/๋ฌธ์„œ๋ฅผ **์ˆ˜์ •ํ•˜์ง€ ์•Š์œผ๋ฉฐ**, ์‚ฐ์ถœ๋ฌผ์€ ๋ณธ ๊ณ„ํš์„œ์ž…๋‹ˆ๋‹ค) - **๊ธฐ์ค€ ์ปค๋ฐ‹**: `31b2d70`, ์ž‘์—… ํŠธ๋ฆฌ clean - **์„ ํ–‰ ๋ฆฌ๋น„์ „**: `fea5f1b2` (Rev.1) โ† ๋ณธ ๋ฌธ์„œ๊ฐ€ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค - **ํŒ์ • ๋Œ€์ƒ ๋ฆฌ๋ทฐ**: `2b8e8ef2` (agy, `[VERDICT: PASS WITH CHALLENGE]`) โ€” C-1 ํ—ค๋“œ๋ฆฌ์Šค `max_columns` ์šฐํšŒ / C-2 ๋ฏธ์ •์˜ ํ—ฌํผ - **ํ…Œ์ŠคํŠธ**: ํ˜„์žฌ **330 passed** โ†’ ์˜ˆ์ƒ **333** (Rev.1 ์˜ 332 ์—์„œ C-3 ์ถ”๊ฐ€) --- ## A. ๋ฆฌ๋ทฐ ํŒ์ • (Adjudication of Challenge `2b8e8ef2`) ### A-0. ํŒ์ • ์š”์•ฝ | ์ฑŒ๋ฆฐ์ง€ | ํŒ์ • | ๊ทผ๊ฑฐ | |---|:---:|---| | **C-1** ํ—ค๋“œ๋ฆฌ์Šค๊ฐ€ `max_columns` ๋ฅผ ์šฐํšŒ | โœ… **์ „๋ฉด ์ˆ˜์šฉ โ€” ์žฌํ˜„ ๋ฐ ์ฒ˜๋ฐฉ ๊ฒ€์ฆ ์™„๋ฃŒ** | `max_columns=2` + ํ—ค๋“œ๋ฆฌ์Šค N=4ยท6ยท8 ์ด ์ „๋ถ€ `right` ๋กœ ์—ด์„ ๋ฌดํ•œ ์ฆ์‹(์‹ค์ธก). GUI ๋Œ€์กฐ๊ตฐ N=4 ๋Š” `overflow`. ์ œ์•ˆ๋œ ํŒจ์น˜๋ฅผ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ์ „ ํ–‰๋ ฌ ๊ฒ€์ฆ | | **C-2** `_four_panes_two_columns()` ๋ฏธ์ •์˜ | โœ… **์ˆ˜์šฉ โ€” ๊ฐ™์€ ์ข…๋ฅ˜์˜ ์˜ค๋ฅ˜๊ฐ€ ํ•˜๋‚˜ ๋” ์žˆ์—ˆ์Œ** | ์ง€์ ๋Œ€๋กœ ๋ฏธ์ •์˜. ์ถ”๊ฐ€๋กœ Rev.1 ์Šค๋‹ˆํŽซ์˜ **`SKILLS_DIR` ๋„ ๋ฏธ์ •์˜**์˜€์Œ(ํŒŒ์ผ์— module-level ์ƒ์ˆ˜ ์—†์Œ). ์ œ์•ˆ ํ—ฌํผ์˜ `-> Dict[str, Any]` ํžŒํŠธ๋Š” `typing` import ์—†์ด๋Š” **def ์‹œ์  NameError**(์‹ค์ธก) | | **C-3** ํ—ค๋“œ๋ฆฌ์Šค ์—ด ์ƒํ•œ ๊ฐ€๋“œ ์‹ ์„ค | โœ… **์ˆ˜์šฉ โ€” ๋ช…์นญยท๋ฌธ์•ˆ๋งŒ ์ •๋ฐ€ํ™”** | ์ฑ„ํƒ. ๋‹ค๋งŒ "strictly enforced" ๋Š” ์‹ค์ œ ์˜๋ฏธ๋ณด๋‹ค ๊ฐ•ํ•จ โ€” ยงA-2 ์ฐธ์กฐ | ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ง€์ ํ•œ ๋‘ ํ•ญ๋ชฉ์€ ๋ชจ๋‘ ์‹ค์žฌํ•˜๋ฉฐ, **C-1 ์€ Rev.1 ์ด ๋†“์นœ ๊ตฌ์กฐ์  ๊ฒฐํ•จ**์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ์žฌํ˜„ยท๊ฒ€์ฆํ•˜๊ณ , ๋ฆฌ๋ทฐ์–ด๊ฐ€ ๋‹ค๋ฃจ์ง€ ์•Š์€ ๋‘ ๊ฐ€์ง€๋ฅผ ๋ง๋ถ™์—ฌ ์ •๋ฐ€ํ™”ํ•ฉ๋‹ˆ๋‹ค. --- ### A-1. C-1 ์žฌํ˜„ ๋ฐ ์ฒ˜๋ฐฉ ๊ฒ€์ฆ #### (1) ๊ฒฐํ•จ ์žฌํ˜„ `max_columns=2` ๋ฅผ ์ค€ ์ƒํƒœ์—์„œ ํ—ค๋“œ๋ฆฌ์Šค ํŽ˜์ธ ์ˆ˜๋ฅผ ๋Š˜๋ ค๊ฐ€๋ฉฐ ์ธก์ •: | N (ํ—ค๋“œ๋ฆฌ์Šค) | ํ˜„์žฌ ๋™์ž‘ | GUI ๋™๋“ฑ ์ƒํ™ฉ | |---|---|---| | 2 | `right` (2๋ฒˆ์งธ ์—ด ๊ฐœ๋ฐฉ) | `right` โœ… ์ผ์น˜ | | 3 | `down` | `down` โœ… ์ผ์น˜ | | **4** | ๐Ÿ”ด **`right`** (3๋ฒˆ์งธ ์—ด ๊ฐœ๋ฐฉ) | ๐ŸŸข **`overflow` / `max_columns_reached`** | | 5 | `down` | `down` (`fill_singleton_column`) โœ… | | **6, 8** | ๐Ÿ”ด **`right`** (์—ด ๋ฌดํ•œ ์ฆ์‹) | `overflow` | `is_headless` ๋ถ„๊ธฐ๊ฐ€ ์—ด ๊ทธ๋ฃนํ•‘๊ณผ `max_columns` ๊ฒ€์‚ฌ๋ณด๋‹ค **๋จผ์ € return** ํ•˜๋ฏ€๋กœ ์ƒํ•œ์ด ํ•œ ๋ฒˆ๋„ ํ‰๊ฐ€๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐ์–ด์˜ ๋ถ„์„์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. #### (2) ์ œ์•ˆ ํŒจ์น˜ ์ „ ํ–‰๋ ฌ ๊ฒ€์ฆ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ œ์‹œํ•œ `current_cols = n // 2` + even ๋ถ„๊ธฐ ๊ฒ€์‚ฌ๋ฅผ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๊ตฌํ˜„ํ•ด `max_columns` ร— N ์ „ ์กฐํ•ฉ์„ ํ™•์ธ: ``` max_columns=None -> N=2:righ N=3:down N=4:righ N=5:down N=6:righ N=7:down N=8:righ max_columns=1 -> N=2:over N=3:down N=4:over N=5:down N=6:over N=7:down N=8:over max_columns=2 -> N=2:righ N=3:down N=4:over N=5:down N=6:over N=7:down N=8:over max_columns=3 -> N=2:righ N=3:down N=4:righ N=5:down N=6:over N=7:down N=8:over ``` - `max_columns=None` ํ–‰์ด **ํ˜„ํ–‰๊ณผ ์™„์ „ํžˆ ๋™์ผ** โ†’ ๊ธฐ์กด `test_headless_0x0_transitions` ๊ฐ€ ๊นจ์ง€์ง€ ์•Š์Œ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค. - `max_columns=K` ๋Š” ์ •ํ™•ํžˆ K๋ฒˆ์งธ ์—ด๊นŒ์ง€ ํ—ˆ์šฉํ•˜๊ณ  K+1๋ฒˆ์งธ๋ฅผ ์—ด๋ ค๋Š” ์‹œ์ ์— overflow ํ•ฉ๋‹ˆ๋‹ค. ์ฒ˜๋ฐฉ์„ ๊ทธ๋Œ€๋กœ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค. ### A-2. ์ •๋ฐ€ํ™” โ‘  โ€” `max_columns` ๋Š” **๋ถˆ๋ณ€์‹์ด ์•„๋‹ˆ๋ผ ์„ฑ์žฅ ๊ฐ€๋“œ**๋‹ค ๋ฆฌ๋ทฐ์–ด๋Š” C-3 ํ…Œ์ŠคํŠธ๋ฅผ *"max_columns is strictly enforced"* ๋กœ ๊ธฐ์ˆ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ธก๋œ ์˜๋ฏธ๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅด๋ฉฐ, ์ด ์ฐจ์ด๊ฐ€ ๋ฆฌ๋ทฐ์–ด์˜ "even ๋ถ„๊ธฐ์—์„œ๋งŒ ๊ฒ€์‚ฌ" ์„ ํƒ์ด ์˜ณ์€ **์ด์œ **์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. GUI ๋ชจ๋“œ์—์„œ `max_columns=2` ์ธ๋ฐ ์ด๋ฏธ 3๋ฒˆ์งธ ์—ด์— ์™ธํ†จ์ด ํŽ˜์ธ์ด ์žˆ๋Š” 5-ํŽ˜์ธ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ๋„ฃ์œผ๋ฉด: ``` 5 panes / singleton : direction=down overflow=False reason=fill_singleton_column ``` ์ด๋ฏธ ์ƒํ•œ์„ ๋„˜๊ธด ์ƒํƒœ์—ฌ๋„ **overflow ๋ฅผ ๋‚ด์ง€ ์•Š๊ณ  ๊ธฐ์กด ์—ด์„ ์ฑ„์›๋‹ˆ๋‹ค**. ์ƒํ•œ์€ "์ƒˆ ์—ด์„ ์—ฌ๋Š” ๊ฒƒ"์„ ๋ง‰์„ ๋ฟ, ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์—ด์„ ์‚ฌํ›„์— ์—†์•จ ์ˆ˜๋Š” ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์™ธํ†จ์ด ํŽ˜์ธ์„ ๋ฐฉ์น˜ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ฑ„์šฐ๋Š” ํŽธ์ด ๊ณต๊ฐ„ ํšจ์œจ์ด ๋‚ซ์Šต๋‹ˆ๋‹ค. ํ—ค๋“œ๋ฆฌ์Šค์˜ ํ™€์ˆ˜ ๋ถ„๊ธฐ(`down`)๊ฐ€ ์ƒํ•œ์„ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์€ GUI ์˜ `fill_singleton_column` ๊ณผ **์ •ํ™•ํžˆ ๊ฐ™์€ ๊ทœ์น™**์ž…๋‹ˆ๋‹ค. ์ฆ‰ ๋ฆฌ๋ทฐ์–ด์˜ ์ฒ˜๋ฐฉ์€ ์ž„์˜์˜ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ **GUI ์™€์˜ ๋Œ€์นญ์„ ๋ณต์›**ํ•˜๋Š” ๊ฒƒ์ด๋ฉฐ, ์ด ์ ์„ ์ฃผ์„๊ณผ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์— ๋‚จ๊ฒจ์•ผ ๋‹ค์Œ ๋…์ž๊ฐ€ "ํ™€์ˆ˜๋Š” ์™œ ๊ฒ€์‚ฌ ์•ˆ ํ•˜๋‚˜"๋ฅผ ๋‹ค์‹œ ๋ฌป์ง€ ์•Š์Šต๋‹ˆ๋‹ค. โ†’ C-3 ํ…Œ์ŠคํŠธ ์ด๋ฆ„/๋…์ŠคํŠธ๋ง์„ `strictly enforced` ๋Œ€์‹  **"opening a new column is blocked; filling an existing one is not"** ์ทจ์ง€๋กœ ๊ธฐ์ˆ ํ•˜๋„๋ก ยง3.2 ์— ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ### A-3. ์ •๋ฐ€ํ™” โ‘ก โ€” `n // 2` ๋Š” ์ธก์ •์ด ์•„๋‹ˆ๋ผ **์ถ”๋ก **์ด๋‹ค GUI ๊ฒฝ๋กœ๋Š” ํŽ˜์ธ์˜ x ์ขŒํ‘œ๋กœ ์—ด์„ **์…‰๋‹ˆ๋‹ค**(ground truth). ํ—ค๋“œ๋ฆฌ์Šค์—๋Š” ์ขŒํ‘œ๊ฐ€ ์—†์œผ๋ฏ€๋กœ `n // 2` ๋กœ **์ถ”์ •**ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐ’์€ ์„ฑ๊ฒฉ์ด ๋‹ค๋ฅด๋ฉฐ, ์ถ”์ •์€ ๊ต๋Œ€ ๋ถˆ๋ณ€์‹(ํ™€์ˆ˜โ†’down, ์ง์ˆ˜โ†’right)์ด ๊ทธ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์—ˆ์„ ๋•Œ๋งŒ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ธ์ด ๋‹ซํ˜€ ํ˜•์ƒ์ด ์–ด๊ธ‹๋‚œ ๊ฒฝ์šฐ(์˜ˆ: 2ร—2 ๊ทธ๋ฆฌ๋“œ์—์„œ ํ•˜๋‚˜๊ฐ€ ๋‹ซํ˜€ N=3)๋Š” `n // 2 = 1` ๋กœ ์‹ค์ œ ์—ด ์ˆ˜(2)๋ฅผ ๊ณผ์†Œํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ **ํ™€์ˆ˜๋Š” ์–ด์ฐจํ”ผ `down` ์œผ๋กœ ํก์ˆ˜**๋˜๊ณ , ๋‹ค์Œ ์ง์ˆ˜ N=4 ์—์„œ `n // 2 = 2` ๊ฐ€ ๋˜์–ด **์ž๊ธฐ ๊ต์ •**๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‹ค์‚ฌ์šฉ์ƒ ์•ˆ์ „ํ•˜์ง€๋งŒ, ์ด ๊ทผ๊ฑฐ๋ฅผ ์ฝ”๋“œ ์ฃผ์„์— ๋‚จ๊ธฐ์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ ์‚ฌ๋žŒ์ด "์™œ ์—ด์„ ์„ธ์ง€ ์•Š๊ณ  ๋‚˜๋ˆ„๊ธฐ๋ฅผ ํ•˜๋А๋ƒ"๋กœ ๋˜๋Œ๋ฆด ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ยง3.1 ๊ตฌํ˜„ ์‚ฌ์–‘์— ์ฃผ์„ ๋ฌธ์•ˆ์„ ํฌํ•จํ–ˆ์Šต๋‹ˆ๋‹ค. ### A-4. ์ •๋ฐ€ํ™” โ‘ข โ€” ์˜ํ–ฅ๋„ ์ •์ •, ๊ทธ๋Ÿฌ๋‚˜ **๊ฐ™์€ ์ปค๋ฐ‹์—์„œ ๊ณ ์ณ์•ผ ํ•˜๋Š” ์ด์œ ** ๋ฆฌ๋ทฐ์–ด๋Š” C-1 ์„ `Critical` ๋กœ ๋ถ„๋ฅ˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ •ํ™•ํžˆ๋Š” **`--max-cols` ๊ธฐ๋ณธ๊ฐ’์ด `None` ์ด๋ผ ์•„๋ฌด๋„ opt-in ํ•˜์ง€ ์•Š์€ ์ง€๊ธˆ์€ ์ž ๋ณต ์ƒํƒœ**์ด๋ฉฐ, ํ˜„์žฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐœ์ƒ ์ค‘์ธ ์žฅ์• ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด๊ฒƒ์ด ์‹ฌ๊ฐ๋„๋ฅผ ๋‚ฎ์ถ”์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. **Rev.1 ์˜ I-3b ๊ฐ€ ๋ฐ”๋กœ ๊ทธ opt-in ๊ฒฝ๋กœ(`MAM_MAX_PANE_COLS`)๋ฅผ ์‚ด๋ฆฌ๋Š” ์ž‘์—…**์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. C-1 ์„ ํ•จ๊ป˜ ๊ณ ์น˜์ง€ ์•Š๊ณ  I-3b ๋งŒ ์ ์šฉํ•˜๋ฉด, ์ด๋ฒˆ ์ปค๋ฐ‹์ด **๊ฒฐํ•จ์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ์ปค๋ฐ‹**์ด ๋ฉ๋‹ˆ๋‹ค. ์šด์˜์ž๊ฐ€ `MAM_MAX_PANE_COLS=2` ๋ฅผ ์„ค์ •ํ•˜๋Š” ์ˆœ๊ฐ„ GUI ๋Š” ์ƒํ•œ์„ ์ง€ํ‚ค๊ณ  ํ—ค๋“œ๋ฆฌ์Šค๋Š” ๋ฌดํ•œํžˆ ์—ด์„ ๋Š˜๋ฆฌ๋Š” **๋ชจ๋“œ ๊ฐ„ ๋™์ž‘ ๋ถ„๊ธฐ**๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค. โ†’ C-1 ๊ณผ I-3b ๋Š” **๋ถ„๋ฆฌ ๋ถˆ๊ฐ€**ํ•˜๋ฉฐ, ยง6 ์‹คํ–‰ ์ˆœ์„œ์—์„œ ๊ฐ™์€ ๋‹จ๊ณ„๋กœ ๋ฌถ์—ˆ์Šต๋‹ˆ๋‹ค. ### A-5. C-2 ์ˆ˜์šฉ โ€” ๊ทธ๋ฆฌ๊ณ  ๊ฐ™์€ ์ข…๋ฅ˜์˜ ์˜ค๋ฅ˜๊ฐ€ ํ•˜๋‚˜ ๋” ์žˆ์—ˆ๋‹ค ๋ฆฌ๋ทฐ์–ด ์ง€์ ๋Œ€๋กœ `_four_panes_two_columns()` ๋Š” ์–ด๋””์—๋„ ์—†์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— Rev.1 ์Šค๋‹ˆํŽซ์˜ ๊ฒฐํ•จ ๋‘ ๊ฐ€์ง€๋ฅผ ์Šค์Šค๋กœ ๋ง๋ถ™์ž…๋‹ˆ๋‹ค. 1. **`SKILLS_DIR` ๋„ ๋ฏธ์ •์˜์˜€์Šต๋‹ˆ๋‹ค.** `tests/test_layout.py` ์—๋Š” module-level ์ƒ์ˆ˜๊ฐ€ ํ•˜๋‚˜๋„ ์—†๊ณ , ๊ธฐ์กด `test_cli_invocation_pipe` ๋Š” ํ…Œ์ŠคํŠธ ๋‚ด๋ถ€์—์„œ `skills_dir = os.path.abspath(".agents/skills")` ๋ฅผ ๋งŒ๋“ค์–ด ์”๋‹ˆ๋‹ค. Rev.1 ์Šค๋‹ˆํŽซ์€ ์ •์˜๋˜์ง€ ์•Š์€ ๋‘ ์ด๋ฆ„์— ์˜์กดํ–ˆ์Šต๋‹ˆ๋‹ค. 2. **๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ œ์•ˆํ•œ ํ—ฌํผ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋„ ๊ทธ๋Œ€๋กœ๋Š” ๊นจ์ง‘๋‹ˆ๋‹ค.** `def _four_panes_two_columns() -> Dict[str, Any]:` ๋Š” `typing` import ์—†์ด๋Š” **์ •์˜ ์‹œ์ ์—** ํ„ฐ์ง‘๋‹ˆ๋‹ค. ``` $ python -c "exec('def f() -> Dict[str, Any]:\n return {}\n')" NameError at def time: name 'Dict' is not defined ``` `tests/test_layout.py` ๋Š” `typing` ์„ import ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ํƒ€์ž… ํžŒํŠธ๋ฅผ ๋นผ๊ฑฐ๋‚˜ import ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ยง3.2 ๋Š” ํžŒํŠธ๋ฅผ ๋นผ๋Š” ์ชฝ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค(ํŒŒ์ผ ์–ด๋””์—๋„ ํƒ€์ž… ํžŒํŠธ๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ๊ด€๋ก€์™€ ์ผ์น˜). --- ## B. Rev.1 โ†’ Rev.2 ๋ณ€๊ฒฝ ์š”์•ฝ | # | ๋ณ€๊ฒฝ | ์ถœ์ฒ˜ | |---|---|---| | C-1 | **`compute_2xk_layout` ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ์— `max_columns` ๊ฒ€์‚ฌ ์ถ”๊ฐ€** โ€” I-3b ์™€ ๋™์ผ ๋‹จ๊ณ„๋กœ ๋ฌถ์Œ | ์ฑŒ๋ฆฐ์ง€ C-1 + A-4 | | C-2 | ํ—ค๋“œ๋ฆฌ์Šค ์ƒํ•œ ์ถ”๋ก  ๊ทผ๊ฑฐ(`n // 2`)์™€ GUI ๋Œ€์นญ์„ฑ์„ **์ฝ”๋“œ ์ฃผ์„์œผ๋กœ ๋ช…๋ฌธํ™”** | A-2 / A-3 | | C-3 | ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ์Šค๋‹ˆํŽซ์—์„œ **`_four_panes_two_columns()` ์™€ `skills_dir` ์„ ์‹ค์ œ๋กœ ์ •์˜**, ํƒ€์ž… ํžŒํŠธ ์ œ๊ฑฐ | ์ฑŒ๋ฆฐ์ง€ C-2 + A-5 | | C-4 | **`test_headless_max_columns_growth_guard` ์‹ ์„ค** (C-3 ์ฑ„ํƒ, ๋ช…์นญยท๋…์ŠคํŠธ๋ง ์ •๋ฐ€ํ™”) โ†’ 332 โ†’ **333** | ์ฑŒ๋ฆฐ์ง€ C-3 + A-2 | | C-5 | ๋ฎคํ…Œ์ด์…˜ ์ˆ˜์šฉ ๊ธฐ์ค€์— **ํ—ค๋“œ๋ฆฌ์Šค ์ƒํ•œ 2์ข…** ์ถ”๊ฐ€ (6์ข… โ†’ 8์ข…) | C-1 | | C-6 | ยง6 ์‹คํ–‰ ์ˆœ์„œ์—์„œ C-1 ๊ณผ I-3b ๋ฅผ **๋ถ„๋ฆฌ ๋ถˆ๊ฐ€**๋กœ ๋ช…์‹œ | A-4 | Rev.1 ์˜ ยง1 ์‹ค์ธก ์›์žฅ(M-1~M-14), ยง2 I-2 ์‚ฌ์–‘, ยง3.1 `focused` ์ œ๊ฑฐ, ยง3.2 `--max-cols` env ๋ฐฐ์„  ๊ฒฐ์ •(bash 3.2 ๊ทผ๊ฑฐ ํฌํ•จ), ยง3.3 ์•ต์ปค ์ฃผ์„, ยง4 ๋ฌธ์„œํ™”๋Š” ๋ฆฌ๋ทฐ์—์„œ ์Šน์ธ๋˜์—ˆ์œผ๋ฉฐ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. --- ## 0. ์š”์•ฝ I-1(`_pane_quiescent` ์ฃผ์„)์€ **์ด๋ฏธ `31b2d70` ์—์„œ ํ•ด๊ฒฐ**๋˜์—ˆ์Šต๋‹ˆ๋‹ค(M-1). ๋ณธ ๊ณ„ํš์˜ ๋ฒ”์œ„๋Š” I-2 ์™€ I-3 ์ด๋ฉฐ, ์—ฌ๊ธฐ์— ๋ฆฌ๋ทฐ๊ฐ€ ๋ฐœ๊ตดํ•œ **C-1(ํ—ค๋“œ๋ฆฌ์Šค `max_columns` ์šฐํšŒ)** ์ด ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. - **I-2 ๋Š” ์‹ค์ธก ๊ฐ€๋Šฅํ•œ ๊ณ„์•ฝ์„ ์„ธ์šฐ๋Š” ์ผ**์ž…๋‹ˆ๋‹ค. ํ—ค๋“œ๋ฆฌ์Šค ์กฐ๊ธฐ ํƒˆ์ถœ(โ‰ˆ1.2 s)์€ ํ˜„์žฌ ์–ด๋–ค ๋‹จ์–ธ์—๋„ ๊ฑธ๋ ค ์žˆ์ง€ ์•Š์•„, ์ œ๊ฑฐํ•ด๋„ 6/6 ์ดˆ๋ก์ธ ์ฑ„๋กœ ์ง€์—ฐ๋งŒ 10๋ฐฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค(M-4). ๊ธฐ๋Šฅ ๋‹จ์–ธ์œผ๋กœ๋Š” ์žก์„ ์ˆ˜ ์—†๊ณ  **์‹œ๊ฐ„ ๋‹จ์–ธ๋งŒ์ด** ์žก์Šต๋‹ˆ๋‹ค. - **I-3 ๋Š” ์ฃฝ์€ ํ‘œ๋ฉด์„ ์ •๋ฆฌํ•˜๋Š” ์ผ**์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ทธ์ค‘ `--max-cols` ๋ฅผ ๋˜์‚ด๋ฆฌ๋Š” ์ž‘์—…์ด **C-1 ๊ฒฐํ•จ์„ ํ™œ์„ฑํ™”**ํ•˜๋ฏ€๋กœ, ๋‘ ์ž‘์—…์€ ๋ฐ˜๋“œ์‹œ ํ•จ๊ป˜ ๊ฐ‘๋‹ˆ๋‹ค. --- ## 1. ์‹ค์ธก ์›์žฅ (Measurement Ledger) | # | ๊ฒ€์ฆ | ๋ฐฉ๋ฒ• | ๊ฒฐ๊ณผ | |---|---|---|---| | M-1 | I-1 ์„ ํ–‰ ํ•ด๊ฒฐ ์—ฌ๋ถ€ | `lib.sh:1578` | ๐ŸŸข `# empty_giveup: $SKS_EMPTY_GIVEUP (default: 3)` โ€” ์ด๋ฏธ ์ •์ •๋จ | | M-2 | ํ˜„์žฌ ์Šค์œ„ํŠธ | `pytest tests/ -q` | **330 passed** | | **M-3** | **ํ—ค๋“œ๋ฆฌ์Šค ์ •์ƒ ์ง€์—ฐ** | ๋…๋ฆฝ ํ”„๋กœ๋ธŒ 5ํšŒ, `/usr/bin/time -p` | **1.22 / 1.24 / 1.24 / 1.23 / 1.24 s** (ฯƒ โ‰ˆ 0.01 s) | | **M-4** | **ํ—ค๋“œ๋ฆฌ์Šค ํšŒ๊ท€ ์ง€์—ฐ** | ์กฐ๊ธฐ giveup ์ œ๊ฑฐ ํ›„ 3ํšŒ | **10.21 / 10.25 / 10.21 s** โ€” rc=0 ์ด๊ณ  RPC ๋„ ํ˜ธ์ถœ๋จ(**๊ธฐ๋Šฅ ๋‹จ์–ธ ๊ฒ€์ถœ ๋ถˆ๊ฐ€**) | | M-5 | B-19 ์Šค์œ„ํŠธ ์†Œ์š” | `--durations=6` | 6 passed / 4.93 s. headless **1.20 s** | | M-6 | bash ๋นˆ ๋ฐฐ์—ด + `set -u` | ์‹œ์Šคํ…œ bash **3.2.57** | ๐Ÿ”ด `"${a[@]}"` โ†’ `unbound variable`. ๐ŸŸข `${a[@]+"${a[@]}"}` ์ •์ƒ | | M-7 | `--max-cols` CLI ํ˜„์žฌ ๋™์ž‘ | 4-pane 2์—ด + `--max-cols 2` | ๐ŸŸข `overflow p3` / `max_columns_reached` | | M-8 | ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ƒ์† ์„ ๋ก€ | `MAM_MIN_PANE_COLS=60` ๋งŒ ์„ค์ • | ๐ŸŸข ํ”Œ๋ž˜๊ทธ ์—†์ด ๋ฐ˜์˜๋จ | | M-9 | `extract_panes_and_focus` ํ˜ธ์ถœ์ฒ˜ | ์ „์—ญ grep | `layout.py:78` 1๊ณณ. `tests/test_layout.py:9` ๋Š” **import ๋งŒ** | | M-10 | `PaneInfo.focused` ํŒ๋…์ฒ˜ | `grep -rn "\.focused\b"` | **0๊ฑด** | | M-11 | `PaneInfo` ์ด๋ฆ„ ์ถฉ๋Œ | `tests/fixtures/herdr_contract.json` | herdr RPC ํƒ€์ž…. **๋ฌด๊ด€, ๊ฑด๋“œ๋ฆฌ์ง€ ๋ง ๊ฒƒ** | | M-12 | `sample_pane` ์˜ ์ •์ฒด | `lib.sh:415-427` | ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ **์ฒซ ๋ฒˆ์งธ pane** โ€” ํฌ์ปค์Šค ๋ฌด๊ด€ | | M-13 | ๋ ˆ์ด์•„์›ƒ env ๋ฌธ์„œํ™” | `grep -c โ€ฆ .mam.env.example` | **0** โ€” `MAM_MIN_PANE_COLS`/`ROWS` ๋ฏธ๋ฌธ์„œํ™” | | M-14 | ํ…Œ์ŠคํŠธ import ๊ฒฝ๋กœ | `tests/conftest.py:10-12` | `.agents/skills` ๋ฅผ `sys.path` ์ฃผ์ž… | | **M-15** | **C-1 ์žฌํ˜„** | ํ—ค๋“œ๋ฆฌ์Šค N=2..8 ร— `max_columns=2` | ๐Ÿ”ด **N=4ยท6ยท8 ์ „๋ถ€ `right`** โ€” ์ƒํ•œ ๋ฏธํ‰๊ฐ€ | | **M-16** | **GUI ๋Œ€์กฐ๊ตฐ** | 4-pane 2์—ด ร— `max_columns=2` | ๐ŸŸข `overflow` / `max_columns_reached` | | **M-17** | **GUI ์™ธํ†จ์ด ์—ด ๊ฑฐ๋™** | 5-pane(2์—ด+์™ธํ†จ์ด) ร— `max_columns=2` | `down` / `fill_singleton_column` โ€” **์ƒํ•œ์€ ์„ฑ์žฅ ๊ฐ€๋“œ**(A-2) | | **M-18** | **C-1 ํŒจ์น˜ ์ „ ํ–‰๋ ฌ** | ํ”„๋กœํ† ํƒ€์ž… ร— `max_columnsโˆˆ{None,1,2,3}` ร— N=2..8 | `None` ํ–‰์ด **ํ˜„ํ–‰๊ณผ ๋™์ผ** โ†’ ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์•ˆ์ „ | | **M-19** | **`_four_panes_two_columns` ์กด์žฌ ์—ฌ๋ถ€** | `grep -rn tests/` | **์—†์Œ**. ํŒŒ์ผ์— module-level ํ—ฌํผ๊ฐ€ **0๊ฐœ**, ์ „ ํ…Œ์ŠคํŠธ๊ฐ€ ์ธ๋ผ์ธ ์„ ์–ธ | | **M-20** | **๋ฏธimport ํƒ€์ž… ํžŒํŠธ** | `exec("def f() -> Dict[str, Any]: ...")` | **์ •์˜ ์‹œ์  NameError** | --- ## 2. I-2 โ€” ํ—ค๋“œ๋ฆฌ์Šค ์กฐ๊ธฐ ํƒˆ์ถœ ์ง€์—ฐ์„ ๊ณ„์•ฝ์œผ๋กœ ๊ณ ์ • *(Rev.1 ยง2 ์—์„œ ๋ณ€๊ฒฝ ์—†์Œ โ€” ๋ฆฌ๋ทฐ ์Šน์ธ)* ### 2.1 ์™œ ์‹œ๊ฐ„ ๋‹จ์–ธ์ด์–ด์•ผ ํ•˜๋Š”๊ฐ€ ์กฐ๊ธฐ giveup ์„ ์ œ๊ฑฐํ•ด๋„ `send_keys_safe` ๋Š” **rc=0 ์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  `agent prompt` ๋„ ํ˜ธ์ถœ**ํ•ฉ๋‹ˆ๋‹ค(M-4). ํ˜„ํ–‰ ๊ธฐ๋Šฅ ๋‹จ์–ธ์ด ์ „๋ถ€ ํ†ต๊ณผํ•˜๊ณ  ๋‹ฌ๋ผ์ง€๋Š” ๊ฒƒ์€ **1.2 s โ†’ 10.2 s** ๋ฟ์ž…๋‹ˆ๋‹ค. ### 2.2 ๊ฒฝ๊ณ„๊ฐ’ โ€” ์‹ค์ธก ๊ทผ๊ฑฐ | ์ƒํƒœ | n | ๋ฒ”์œ„ | |---|---|---| | ์ •์ƒ | 5 | **1.22 โ€“ 1.24 s** | | ํšŒ๊ท€ | 3 | **10.21 โ€“ 10.25 s** | `SKS_EMPTY_GIVEUP=3`, `interval=0.5` โ†’ 3๋ฒˆ์งธ ๊ณต๋ฐฑ ์บก์ฒ˜์—์„œ sleep ์—†์ด ์ฆ‰์‹œ `return 2` ํ•˜๋ฏ€๋กœ sleep 2ํšŒ = 1.0 s + bash ๊ธฐ๋™ 0.2 s. ํšŒ๊ท€ ์‹œ 20 ร— 0.5 = 10.0 s. ํ—ค๋“œ๋ฆฌ์Šค ๊ฒฝ๋กœ๋Š” `_pane_capture` ๊ฐ€ python3 ๋ฅผ ๋„์šฐ์ง€ ์•Š์•„ ์ธก์ •์ด ๊ฑฐ์˜ ์ˆœ์ˆ˜ sleep ์ž…๋‹ˆ๋‹ค(ฯƒ โ‰ˆ 0.01 s). **์ฑ„ํƒ: 5.0 s** โ€” ์ •์ƒ ๋Œ€๋น„ 4๋ฐฐ ์—ฌ์œ , ํšŒ๊ท€ ๋Œ€๋น„ 2๋ฐฐ ๋งˆ์ง„. ### 2.3 ๊ตฌํ˜„ ์‚ฌ์–‘ ```python def test_bug4_headless_unobservable_fast_path(tmp_path): """Verify Bug 4 / R-1 + I-2: in headless mode where capture-pane is empty, send_keys_safe bypasses dialogs and succeeds immediately via the RPC fast-path. The elapsed-time bound is a contract, not a nicety: removing the SKS_EMPTY_GIVEUP early exit leaves every functional assertion green and only changes the wall clock (measured 1.22s -> 10.21s), so this is the sole assertion that can detect that regression. """ test_script = f"""...""" # ๋ณธ๋ฌธ ๋ณ€๊ฒฝ ์—†์Œ # SKS_* ๋Š” pin ์ด ์•„๋‹ˆ๋ผ '์ œ๊ฑฐ'ํ•œ๋‹ค: lib.sh ์˜ ๊ธฐ๋ณธ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ์ ์šฉ๋˜์–ด์•ผ # ๊ธฐ๋ณธ๊ฐ’ ์ž์ฒด์˜ ํšŒ๊ท€๋ฅผ ํƒ์ง€ํ•  ์ˆ˜ ์žˆ๊ณ , ๋™์‹œ์— ๊ฐœ๋ฐœ์ž ์…ธ์— ๋‚จ์•„ ์žˆ๋Š” # ๊ฐ’ ๋•Œ๋ฌธ์— ์‹œ๊ฐ„ ๋‹จ์–ธ์ด ํ”๋“ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. env = {k: v for k, v in os.environ.items() if k not in ("SKS_QUIESCENT_TRIES", "SKS_QUIESCENT_INTERVAL", "SKS_EMPTY_GIVEUP")} t0 = time.perf_counter() res = subprocess.run(["bash", "-c", test_script], capture_output=True, text=True, env=env) elapsed = time.perf_counter() - t0 assert res.returncode == 0, f"Headless send_keys_safe failed: {res.stderr}" assert "HEADLESS_OK" in res.stdout assert elapsed < 5.0, ( f"headless fast-path took {elapsed:.2f}s (limit 5.0s) โ€” the " f"SKS_EMPTY_GIVEUP early exit in _pane_quiescent is likely gone; " f"the full 10s quiescence window was consumed instead") ``` **ํ•„์ˆ˜**: ํŒŒ์ผ ์ƒ๋‹จ `import time` ์ถ”๊ฐ€ / ์ธก์ •์€ `subprocess.run` ๋งŒ ๊ฐ์Œˆ / `SKS_*` ๋Š” **์ œ๊ฑฐ**(pin ๊ธˆ์ง€) / ์‹คํŒจ ๋ฉ”์‹œ์ง€์— ์ธก์ •๊ฐ’๊ณผ ์›์ธ ๊ฐ€์„ค ํฌํ•จ. --- ## 3. I-3 + C-1 โ€” ์ฃฝ์€ ํ‘œ๋ฉด ์ •๋ฆฌ ๋ฐ ํ—ค๋“œ๋ฆฌ์Šค ์ƒํ•œ ๋ณต์› ### 3.1 C-1 โ€” ํ—ค๋“œ๋ฆฌ์Šค `max_columns` ๊ฒ€์‚ฌ (**์‹ ๊ทœ, I-3b ์™€ ๋™์ผ ๋‹จ๊ณ„**) ```python # Check for Headless mode: all panes have width <= 0 or height <= 0 is_headless = all(p.width <= 0 or p.height <= 0 for p in panes) if is_headless: # Headless panes are all 0x0, so columns cannot be counted from geometry # the way the GUI path does. The alternation below (odd -> down, # even -> right) is what builds the grid, so while that invariant holds # the completed-column count is exactly n // 2. If panes were closed and # the shape drifted, an odd n is absorbed by the `down` branch and the # estimate self-corrects at the next even n. n = len(panes) anchor = default_anchor_id or panes[-1].pane_id if n % 2 == 1: # Filling an existing column never opens a new one, so max_columns is # deliberately NOT checked here -- this mirrors the GUI path, where # `fill_singleton_column` also ignores the cap. max_columns is a # growth guard, not an invariant over the existing layout. return LayoutDecision(target_pane_id=anchor, direction="down", reason="headless_odd_down") current_cols = n // 2 if max_columns and current_cols >= max_columns: return LayoutDecision(target_pane_id=anchor, direction="overflow", is_overflow=True, reason="max_columns_reached") return LayoutDecision(target_pane_id=anchor, direction="right", reason="headless_even_right") ``` **๋™์ž‘ ์ค‘๋ฆฝ์„ฑ**: `max_columns` ๊ฐ€ `None` ์ด๋ฉด ๋ถ„๊ธฐ๊ฐ€ ํ†ต์งธ๋กœ ๊ฑด๋„ˆ๋›ฐ์–ด์ ธ ํ˜„ํ–‰๊ณผ ์™„์ „ํžˆ ๋™์ผํ•ฉ๋‹ˆ๋‹ค(M-18). ๊ธฐ์กด `test_headless_0x0_transitions` ๋Š” ์†๋Œ€์ง€ ์•Š์•„๋„ ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. **`reason` ๋ฌธ์ž์—ด**: GUI ์™€ ๋™์ผํ•œ `max_columns_reached` ๋ฅผ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฒฝ๋กœ๊ฐ€ ๊ฐ™์€ ์‚ฌ์œ ๋ฅผ ๋‚ด์•ผ `--json` ์†Œ๋น„์ž์™€ ๋กœ๊ทธ ๋ถ„์„์—์„œ ๋ชจ๋“œ๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๊ณ  ์ง‘๊ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ### 3.2 `PaneInfo.focused` โ€” **์ œ๊ฑฐ** *(Rev.1 ยง3.1 ์œ ์ง€)* ํŒ๋…์ฒ˜ 0๊ฑด(M-10), ํ˜ธ์ถœ์ฒ˜ 1๊ณณ(M-9). `tests/test_layout.py:9,11` ์€ import ๋งŒ ํ•˜๊ณ  ์“ฐ์ง€ ์•Š์œผ๋ฉฐ CI flake8 ๊ฐ€ `--select=E9,F63,F7,F82` ๋ผ F401 ์„ ๋ณด์ง€ ์•Š์•„ ํ†ต๊ณผํ•ด ์™”์Šต๋‹ˆ๋‹ค. **ํ—ค๋“œ๋ฆฌ์Šค ์•ต์ปค๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ๋Œ€์•ˆ์€ ๊ธฐ๊ฐ**: (a) 2ร—K ์—”์ง„์˜ ๊ฐ€์น˜๋Š” ๊ฒฐ์ •๋ก ์ธ๋ฐ ํฌ์ปค์Šค๋Š” ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์ƒํƒœ์ด๊ณ , (b) `lib.sh` ๊ฐ€ ํ•ญ์ƒ `--sample-pane` ๋ฅผ ๋„˜๊ธฐ๋ฏ€๋กœ ๋„๋‹ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์• ์ดˆ์— `sample_pane` ์€ "ํฌ์ปค์Šค๋œ pane" ์ด ์•„๋‹ˆ๋ผ ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ ์ฒซ ๋ฒˆ์งธ pane ์ž…๋‹ˆ๋‹ค(M-12). ```python @dataclass class PaneInfo: pane_id: str x: int y: int width: int height: int # NOTE: no `focused` field. The 2xK engine is deliberately geometry- and # structure-driven so that identical pane sets always yield identical # decisions. Focus is user-interaction state and would make the result # non-deterministic; herdr still reports it in the payload if ever needed. def extract_panes(data: Dict[str, Any]) -> List[PaneInfo]: """Extract the pane list from a herdr layout JSON payload. Accepts all three shapes herdr 0.8 emits: result.layout.panes, result.panes, and a bare top-level panes array. """ ``` ๋™๋ฐ˜: `compute_2xk_layout:78` โ†’ `panes = extract_panes(data)`, ๋ฏธ์‚ฌ์šฉ `Tuple` import ์ •๋ฆฌ, `tests/test_layout.py:9-11` ์˜ ๋ฏธ์‚ฌ์šฉ import ์ œ๊ฑฐ. > โš ๏ธ `tests/fixtures/herdr_contract.json` ๊ณผ `tests/test_herdr_shim_contract.py:70` ์˜ `PaneInfo` ๋Š” **herdr RPC ๊ณ„์•ฝ ํƒ€์ž…**์ž…๋‹ˆ๋‹ค(M-11). ๊ฑด๋“œ๋ฆฌ์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ### 3.3 `--max-cols` env ๋ฐฐ์„  *(Rev.1 ยง3.2 ์œ ์ง€)* CLI ๋Š” ์ด๋ฏธ ์ •์ƒ(M-7)์ด๋‚˜ argparse ๋งŒ env ๊ธฐ๋ณธ๊ฐ’์ด ์—†์–ด ํ”„๋กœ๋•์…˜ ๋ฏธ๋„๋‹ฌ์ž…๋‹ˆ๋‹ค. **`lib.sh` ์กฐ๊ฑด๋ถ€ ๋ฐฐ์—ด ์ „๋‹ฌ์€ ๊ธฐ๊ฐ** โ€” macOS ๊ธฐ๋ณธ bash **3.2.57** ์—์„œ `set -euo pipefail` + ๋นˆ ๋ฐฐ์—ด์€ ์ฆ‰์‚ฌํ•ฉ๋‹ˆ๋‹ค(M-6). `${a[@]+"${a[@]}"}` ์šฐํšŒ๋Š” ๊ฐ€๋Šฅํ•˜๋‚˜ ๋Œ€๋ถ€๋ถ„์ด ๋ชจ๋ฅด๋Š” ๊ด€์šฉ๊ตฌ๋ฅผ ํ•ต์‹ฌ ๊ฒฝ๋กœ์— ์‹ฌ๋Š” ๋Œ€๊ฐ€๊ฐ€ ์ด์ต๋ณด๋‹ค ํฝ๋‹ˆ๋‹ค. **argparse env ๊ธฐ๋ณธ๊ฐ’ ๋ฐฉ์‹์€ `lib.sh` ๋ฅผ ํ•œ ๊ธ€์ž๋„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ ** ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ƒ…๋‹ˆ๋‹ค(M-8 ์„ ๋ก€). ```python def _env_int(*names: str) -> Optional[int]: """First non-empty env var among *names, parsed as int. Bad values are ignored rather than raised: a typo in an operator's shell must not take the whole layout call down (lib.sh would silently fall back to 'right').""" for n in names: raw = os.environ.get(n, "").strip() if raw: try: return int(raw) except ValueError: return None return None parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS")) ``` ๊ธฐ๋ณธ๊ฐ’์€ ๊ณ„์† `None`(์ƒํ•œ ์—†์Œ) โ€” **๋™์ž‘ ์ค‘๋ฆฝ**์ด๋ฉฐ ์šด์˜์ž๊ฐ€ opt-in ํ•  ๋•Œ๋งŒ ์ƒํ•œ์ด ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ### 3.4 ํ—ค๋“œ๋ฆฌ์Šค ์•ต์ปค ์ฃผ์„ ์ •์ • *(Rev.1 ยง3.3 ์œ ์ง€, ยง3.1 ์ฝ”๋“œ์— ํ†ตํ•ฉ๋จ)* `panes[-1]` ํด๋ฐฑ์€ `lib.sh` ๊ฐ€ ํ•ญ์ƒ `--sample-pane` ๋ฅผ ๋„˜๊ธฐ๋ฏ€๋กœ ํ”„๋กœ๋•์…˜์—์„œ ๋„๋‹ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. `no_panes_default` ๋ถ„๊ธฐ(`layout.py:81-82`)์—๋„ ๊ฐ™์€ ์ทจ์ง€์˜ ํ•œ ์ค„์„ ๊ถŒ๊ณ ํ•ฉ๋‹ˆ๋‹ค. ### 3.5 ์‹ ๊ทœ ํ…Œ์ŠคํŠธ 3๊ฑด (C-2 / C-3 ๋ฐ˜์˜) `tests/test_layout.py` ๋Š” **module-level ํ—ฌํผ๊ฐ€ 0๊ฐœ์ด๊ณ  ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ธ๋ผ์ธ ์„ ์–ธ**ํ•ฉ๋‹ˆ๋‹ค(M-19). ์ƒˆ ํ—ฌํผ 1๊ฐœ๋ฅผ ๋„์ž…ํ•˜๋˜ ํŒŒ์ผ ๊ด€๋ก€๋ฅผ ์กด์ค‘ํ•ด ํƒ€์ž… ํžŒํŠธ๋Š” ๋ถ™์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค(M-20 โ€” `typing` ๋ฏธimport ์ƒํƒœ์—์„œ ํžŒํŠธ๋Š” ์ •์˜ ์‹œ์ ์— ํ„ฐ์ง‘๋‹ˆ๋‹ค). ```python def _four_panes_two_columns(): """GUI payload: 2 full columns x 2 rows (4 panes). Shared by the max-cols tests.""" return { "result": { "panes": [ {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}}, {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 100, "height": 40}}, {"pane_id": "p3", "rect": {"x": 100, "y": 0, "width": 100, "height": 40}}, {"pane_id": "p4", "rect": {"x": 100, "y": 40, "width": 100, "height": 40}}, ] } } def test_cli_max_cols_flag_triggers_overflow(): """CLI --max-cols reaches compute_2xk_layout (the lib.sh-facing path).""" payload = json.dumps(_four_panes_two_columns()) skills_dir = os.path.abspath(".agents/skills") # ํŒŒ์ผ ๊ด€๋ก€: ํ…Œ์ŠคํŠธ ๋‚ด๋ถ€์—์„œ ๊ณ„์‚ฐ env = {**os.environ, "PYTHONPATH": skills_dir} res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--min-cols", "30", "--min-rows", "20", "--max-cols", "2", "--json"], input=payload, capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr d = json.loads(res.stdout) assert d["direction"] == "overflow" and d["is_overflow"] assert d["reason"] == "max_columns_reached" def test_env_max_cols_applies_without_flag(): """MAM_MAX_PANE_COLS is honoured with no --max-cols flag, which is exactly how lib.sh invokes the module (lib.sh passes no --max-cols).""" payload = json.dumps(_four_panes_two_columns()) skills_dir = os.path.abspath(".agents/skills") env = {**os.environ, "PYTHONPATH": skills_dir, "MAM_MAX_PANE_COLS": "2"} res = subprocess.run( [sys.executable, "-m", "lib_py.layout", "--min-cols", "30", "--min-rows", "20", "--json"], input=payload, capture_output=True, text=True, env=env) assert res.returncode == 0, res.stderr assert json.loads(res.stdout)["reason"] == "max_columns_reached" def test_headless_max_columns_growth_guard(): """C-1: headless mode must honour max_columns too. A headless 2xK grid completes n // 2 columns, so at n=4 with max_columns=2 a further `right` split would open a third column and must overflow instead. Note the cap blocks *opening* a new column; it does not force an existing over-cap layout to shrink -- the odd-n `down` branch (and the GUI's fill_singleton_column) deliberately ignore it. """ def headless(n): return {"result": {"panes": [ {"pane_id": f"p{i}", "rect": {"x": 0, "y": 0, "width": 0, "height": 0}} for i in range(1, n + 1)]}} d4 = compute_2xk_layout(headless(4), max_columns=2) assert d4.is_overflow and d4.direction == "overflow" assert d4.reason == "max_columns_reached" # ์ƒํ•œ ๋ฏธ๋งŒ์—์„œ๋Š” ๊ณ„์† ์„ฑ์žฅํ•œ๋‹ค d2 = compute_2xk_layout(headless(2), max_columns=2) assert d2.direction == "right" and not d2.is_overflow # ๊ธฐ์กด ์—ด์„ ์ฑ„์šฐ๋Š” ๊ฒƒ์€ ๋ง‰์ง€ ์•Š๋Š”๋‹ค (GUI ์˜ fill_singleton_column ๊ณผ ๋™์ผ ๊ทœ์น™) d3 = compute_2xk_layout(headless(3), max_columns=2) assert d3.direction == "down" and not d3.is_overflow # max_columns ๋ฏธ์ง€์ • ์‹œ ํ˜„ํ–‰ ๋™์ž‘ ์œ ์ง€ (๋™์ž‘ ์ค‘๋ฆฝ์„ฑ) assert compute_2xk_layout(headless(4)).direction == "right" ``` ๋งˆ์ง€๋ง‰ ๋‹จ์–ธ(๋™์ž‘ ์ค‘๋ฆฝ์„ฑ)์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค โ€” C-1 ํŒจ์น˜๊ฐ€ ๊ธฐ์กด ํ—ค๋“œ๋ฆฌ์Šค ๊ต๋Œ€๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์•˜์Œ์„ ๊ฐ™์€ ํ…Œ์ŠคํŠธ ์•ˆ์—์„œ ๋ชป๋ฐ•์Šต๋‹ˆ๋‹ค. > ๊ธฐ์กด `test_max_columns_limit` ์€ ๋™์ผํ•œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ธ๋ผ์ธ์œผ๋กœ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. `_four_panes_two_columns()` ๋กœ ์น˜ํ™˜ํ•˜๋ฉด ์ค‘๋ณต์ด ์ค„์ง€๋งŒ, ํ†ต๊ณผ ์ค‘์ธ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑด๋“œ๋ฆฌ๋Š” ๊ฒƒ์€ ์„ ํƒ ์‚ฌํ•ญ์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค(ยง7 Q-5). --- ## 4. ๋ฌธ์„œํ™” โ€” ๋ ˆ์ด์•„์›ƒ ํŠœ๋„ˆ๋ธ” *(Rev.1 ยง4 ์œ ์ง€)* `.mam.env.example` ์— `MAM_MIN_PANE_COLS` / `MAM_MIN_PANE_ROWS` ๊ฐ€ **ํ•œ ๊ฑด๋„ ์—†์Šต๋‹ˆ๋‹ค**(M-13). ์ง์ „ ์ปค๋ฐ‹์—์„œ `SKS_*` 3์ข…์„ ๋ฌธ์„œํ™”ํ•œ ๊ฒƒ๊ณผ ํ˜•ํ‰์ด ๋งž์ง€ ์•Š๊ณ , `MAM_MAX_PANE_COLS` ๋ฅผ ์ƒˆ๋กœ ์‚ด๋ฆฌ๋ฉด์„œ ์ด ๊ณต๋ฐฑ์„ ๋‘๋ฉด ์‹ ๊ทœ ๋ณ€์ˆ˜๋งŒ ๋ฏธ๋ฌธ์„œํ™”๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ```bash # Minimum columns a pane must retain after a vertical split (2xK layout engine). #default: 60 # MAM_MIN_PANE_COLS=60 # Minimum rows a pane must retain after a horizontal split (2xK layout engine). #default: 20 # MAM_MIN_PANE_ROWS=20 # Maximum number of columns a workspace may grow to before the engine reports # 'overflow' (which makes lib.sh create a fresh workspace instead of splitting). # Applies to both measured (GUI) and headless 0x0 layouts. #default: (unset -> no column cap) # MAM_MAX_PANE_COLS=3 ``` D-7 ์€ **์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์“ฐ๋Š”** ๋ณ€์ˆ˜๋งŒ ๊ฒ€์‚ฌํ•˜๋ฏ€๋กœ ๊นจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. D-21/D-32 ๋Š” `MQTT_*` ๋Œ€์ƒ์ด๋ผ ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค โ€” ๋‹ค๋งŒ ยง6 ์—์„œ ๋ฐฐํฌ ์‹ ์„ ๋„ 31๊ฑด ์žฌํ™•์ธ์„ ์ ˆ์ฐจ์— ๋„ฃ์Šต๋‹ˆ๋‹ค. --- ## 5. ํšŒ๊ท€ ๊ฐ€๋“œ ๋ฐ ์ˆ˜์šฉ ๊ธฐ์ค€ | ๊ฐ€๋“œ | ๋Œ€์ƒ | ๋ฎคํ…Œ์ด์…˜ | ๊ธฐ๋Œ€ | |---|---|---|---| | `test_bug4_headless_unobservable_fast_path` (I-2 ๊ฐ•ํ™”) | ์กฐ๊ธฐ ํƒˆ์ถœ ์ง€์—ฐ | ์กฐ๊ธฐ `return 2` ์ œ๊ฑฐ | **FAIL** | | ๋™ | ๋™ | `SKS_EMPTY_GIVEUP` ๊ธฐ๋ณธ๊ฐ’ 3โ†’20 | **FAIL** | | `test_cli_max_cols_flag_triggers_overflow` (์‹ ๊ทœ) | CLI ๊ฒฝ๋กœ | `--max-cols` argparse ์ธ์ž ์ œ๊ฑฐ | **FAIL** | | `test_env_max_cols_applies_without_flag` (์‹ ๊ทœ) | env ๋ฐฐ์„  | `default=_env_int(...)` โ†’ `default=None` | **FAIL** | | **`test_headless_max_columns_growth_guard`** (์‹ ๊ทœ) | **C-1** | ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ์˜ `max_columns` ๊ฒ€์‚ฌ ์ œ๊ฑฐ | **FAIL** | | ๋™ | **C-1 ๋™์ž‘ ์ค‘๋ฆฝ์„ฑ** | ํ—ค๋“œ๋ฆฌ์Šค ํ™€์ˆ˜ ๋ถ„๊ธฐ์—๋„ ์ƒํ•œ ๊ฒ€์‚ฌ ์ถ”๊ฐ€(๊ณผ์ž‰ ๊ต์ •) | **FAIL** (`d3` ๋‹จ์–ธ) | | ๊ธฐ์กด `test_max_columns_limit` | Python API (GUI) | `max_columns` ๋ถ„๊ธฐ ์‚ญ์ œ | **FAIL** | | ๊ธฐ์กด `test_headless_0x0_transitions` | ํ—ค๋“œ๋ฆฌ์Šค ๊ต๋Œ€ | C-1 ํŒจ์น˜ ์ ์šฉ | **ํ†ต๊ณผ ์œ ์ง€**(ํšŒ๊ท€ ์—†์Œ ํ™•์ธ) | **ํ…Œ์ŠคํŠธ ์ˆ˜ ์˜ˆ์ƒ**: 330 โ†’ **333** (์‹ ๊ทœ 3๊ฑด, I-2 ๋Š” ๊ธฐ์กด ํ…Œ์ŠคํŠธ์— ๋‹จ์–ธ ์ถ”๊ฐ€). --- ## 6. ์‹คํ–‰ ์ˆœ์„œ ๋ฐ ์™„๋ฃŒ ์ •์˜ ``` [1] I-2 ์‹œ๊ฐ„ ๋‹จ์–ธ โ”€โ”€> [2] I-3a focused ์ œ๊ฑฐ โ”€โ”€> [3] C-1 + I-3b (๋ถ„๋ฆฌ ๋ถˆ๊ฐ€) โ”€โ”€> [4] ์ฃผ์„ โ”€โ”€> [5] ๋ฌธ์„œ โ”€โ”€> [6] ๊ฒ€์ฆ import time PaneInfo/extract ์ •๋ฆฌ ํ—ค๋“œ๋ฆฌ์Šค ์ƒํ•œ + env ๋ฐฐ์„  ์•ต์ปค ์ฃผ์„ .mam.env ๋ฎคํ…Œ์ด์…˜ 8์ข… env ํ•„ํ„ฐ๋ง + ์‹ ๊ทœ ํ…Œ์ŠคํŠธ 3๊ฑด 3์ข… ์ถ”๊ฐ€ + 333 ์ „๊ฑด ``` > [!IMPORTANT] > **[3] ์€ ์ชผ๊ฐœ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** I-3b ๊ฐ€ `MAM_MAX_PANE_COLS` opt-in ๊ฒฝ๋กœ๋ฅผ ์‚ด๋ฆฌ๊ณ , C-1 ์ด ๊ทธ ๊ฒฝ๋กœ์˜ ํ—ค๋“œ๋ฆฌ์Šค ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. I-3b ๋งŒ ๋จผ์ € ์ ์šฉํ•˜๋ฉด ์ด๋ฒˆ ์ปค๋ฐ‹์ด **๊ฒฐํ•จ์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ์ปค๋ฐ‹**์ด ๋ฉ๋‹ˆ๋‹ค(A-4). **๋‹จ๊ณ„๋ณ„ ํ™•์ธ** 1. I-2 ์ ์šฉ ์งํ›„ `pytest tests/test_b19_headless_reconcile_fixes.py -q --durations=6` ๋กœ headless ์†Œ์š”๊ฐ€ ์—ฌ์ „ํžˆ โ‰ˆ1.2 s ์ธ์ง€ ํ™•์ธ. 2. I-3a ๋Š” ๊ฐœ๋ช…์ด๋ฏ€๋กœ **ํ˜ธ์ถœ๋ถ€ 1๊ณณ(`layout.py:78`) + ํ…Œ์ŠคํŠธ import 1๊ณณ**๋งŒ ์ˆ˜์ •(M-9). 3. C-1 ์ ์šฉ ํ›„ `MAM_MAX_PANE_COLS` **๋ฏธ์„ค์ • ์ƒํƒœ**์—์„œ ๊ธฐ์กด `test_layout.py` 16๊ฑด ์ „๊ฑด ํ†ต๊ณผ โ†’ ๋™์ž‘ ์ค‘๋ฆฝ์„ฑ ํ™•์ธ. **DoD** 1. `pytest tests/ -q` โ†’ **333 passed**, exit 0. 2. ยง5 ๋ฎคํ…Œ์ด์…˜ 8์ข…์ด ๊ฐ๊ฐ ์ง€์ • ํ…Œ์ŠคํŠธ๋ฅผ FAIL ์‹œํ‚ด์ด ๋กœ๊ทธ๋กœ ํ™•์ธ๋˜๊ณ  ์›๋ณต๋จ. 3. `bash -n .agents/skills/lib.sh`, `py_compile lib_py/layout.py` ํ†ต๊ณผ. 4. `pytest tests/test_deploy_freshness.py -q` โ†’ 31 passed. 5. `grep -rn "\.focused\b" .agents/skills/` โ†’ 0๊ฑด, `grep -n "extract_panes_and_focus" tests/` โ†’ 0๊ฑด. 6. GUI ์™€ ํ—ค๋“œ๋ฆฌ์Šค๊ฐ€ ๊ฐ™์€ `max_columns` ์—์„œ **๊ฐ™์€ ์‹œ์ ์— overflow** ํ•จ์„ ์ˆ˜๋™ ํ™•์ธ(4-pane / `max_columns=2` ์–‘์ชฝ ๋ชจ๋‘ `max_columns_reached`). 7. `git status --short` ์— ์˜๋„ํ•œ 5ํŒŒ์ผ ์™ธ ๋ณ€๊ฒฝ ์—†์Œ. **๊ฒŒ์ดํŠธ**: 2๋ฒˆ ๋ฏธ์ถฉ์กฑ ์‹œ ์ปค๋ฐ‹ ๊ธˆ์ง€. ํŠนํžˆ I-2 ์‹œ๊ฐ„ ๋‹จ์–ธ์€ ์กฐ๊ธฐ giveup ์ œ๊ฑฐ ๋ฎคํ…Œ์ด์…˜์—์„œ, C-1 ๊ฐ€๋“œ๋Š” ํ—ค๋“œ๋ฆฌ์Šค ์ƒํ•œ ๊ฒ€์‚ฌ ์ œ๊ฑฐ ๋ฎคํ…Œ์ด์…˜์—์„œ **๋ฐ˜๋“œ์‹œ FAIL** ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. **๋ฒ”์œ„ ๋ฐ–**: `--min-cols`/`--min-rows` ์˜ `lib.sh` ๋ช…์‹œ ์ „๋‹ฌ ์œ ์ง€ ์—ฌ๋ถ€, ์—ด ์ƒํ•œ ๊ธฐ๋ณธ๊ฐ’ ๋„์ž…(Q-2), `IMPROVEMENTS.md` ํ•ญ๋ชฉ ๋“ฑ๋ก(Q-3), ํ˜ผํ•ฉ ๋ชจ๋“œ(์ผ๋ถ€๋งŒ 0ร—0) ์ฒ˜๋ฆฌ(Q-6). --- ## 7. ์—ด๋ฆฐ ์งˆ๋ฌธ (๋น„์ฐจ๋‹จ) | # | ์งˆ๋ฌธ | ๊ธฐ๋ณธ๊ฐ’(๋ฌด์‘๋‹ต ์‹œ) | |---|---|---| | **Q-1** | I-2 ์ƒํ•œ์„ 5.0 s ๋กœ ํ•  ๊ฒƒ์ธ๊ฐ€? | **5.0 s** ์œ ์ง€ (์‹ค์ธก 1.22โ€“1.24 s ๋Œ€๋น„ 4๋ฐฐ, ํšŒ๊ท€ 10.2 s ๋Œ€๋น„ 2๋ฐฐ) | | **Q-2** | `MAM_MAX_PANE_COLS` ์— ๊ธฐ๋ณธ ์ƒํ•œ์„ ์ค„ ๊ฒƒ์ธ๊ฐ€? | **์ฃผ์ง€ ์•Š์Œ**(`None`). ๊ธฐ๋ณธ๊ฐ’์„ ์ฃผ๋ฉด ๊ธฐ์กด ์›Œํฌ์ŠคํŽ˜์ด์Šค๊ฐ€ ๊ฐ‘์ž๊ธฐ ๋ถ„๊ธฐ | | **Q-3** | `IMPROVEMENTS.md` ์— ๋“ฑ๋กํ•  ๊ฒƒ์ธ๊ฐ€? | **B-20 ํ•ญ๋ชฉ์— ํ›„์† ์ •๋ฆฌ๋กœ 1โ€“2์ค„ ์ถ”๊ฐ€.** ๋‹จ, **C-1 ์€ ๋ณ„๋„ ๋ฌธ์žฅ์œผ๋กœ ๋ช…์‹œ** โ€” ์ž ๋ณต ๊ฒฐํ•จ์ด์—ˆ๊ณ  opt-in ํ™œ์„ฑํ™”์™€ ํ•จ๊ป˜ ๊ณ ์ณ์กŒ๋‹ค๋Š” ์‚ฌ์‹ค์€ ๊ธฐ๋ก ๊ฐ€์น˜๊ฐ€ ์žˆ์Œ | | **Q-4** | `extract_panes_and_focus` ๊ฐœ๋ช…์ด ๋ถ€๋‹ด์Šค๋Ÿฌ์šฐ๋ฉด ์ด๋ฆ„ ์œ ์ง€? | **๊ฐœ๋ช… ๊ถŒ๊ณ **(`extract_panes`). ๋ฐ˜ํ™˜์ด ํŠœํ”Œ์ด ์•„๋‹ˆ๊ฒŒ ๋˜๋ฏ€๋กœ ์ด๋ฆ„์ด ๋‚จ์œผ๋ฉด ๋” ์˜คํ•ด๋ฅผ ๋ถ€๋ฆ„ | | **Q-5** ๐Ÿ†• | ๊ธฐ์กด `test_max_columns_limit` ์„ `_four_panes_two_columns()` ๋กœ ๋ฆฌํŒฉํ„ฐ๋งํ•  ๊ฒƒ์ธ๊ฐ€? | **ํ•˜์ง€ ์•Š์Œ**. ํ†ต๊ณผ ์ค‘์ธ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑด๋“œ๋ฆฌ๋Š” ์œ„ํ—˜ ๋Œ€๋น„ ์ด๋“์ด ์ค‘๋ณต 12์ค„ ์ œ๊ฑฐ๋ฟ | | **Q-6** ๐Ÿ†• | ํ˜ผํ•ฉ ๋ชจ๋“œ(์ผ๋ถ€ ํŽ˜์ธ๋งŒ 0ร—0)๋ฅผ ๋‹ค๋ฃฐ ๊ฒƒ์ธ๊ฐ€? | **์ด๋ฒˆ ๋ฒ”์œ„ ๋ฐ–**. `is_headless` ๊ฐ€ `all(...)` ์ด๋ผ ํ˜ผํ•ฉ์€ GUI ๊ฒฝ๋กœ๋กœ ๋–จ์–ด์ง€๊ณ  0-ํญ ํŽ˜์ธ์ด ํ•œ ์—ด๋กœ ๋ฌถ์ž„. ์‹ค์ œ ๋ฐœ์ƒ ์‚ฌ๋ก€๊ฐ€ ๊ด€์ธก๋˜๋ฉด ๋ณ„๋„ ๊ณผ์ œ๋กœ | --- ## 8. ๋ถ€๋ก โ€” Creator ์ฐฉ์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - [ ] `tests/test_b19_headless_reconcile_fixes.py` ์— `import time` ์ถ”๊ฐ€ - [ ] `test_bug4_headless_unobservable_fast_path` ์— SKS_* ํ™˜๊ฒฝ๋ณ€์ˆ˜ **์ œ๊ฑฐ**(pin ์•„๋‹˜) + `elapsed < 5.0` ๋‹จ์–ธ (ยง2.3) - [ ] ๋ฎคํ…Œ์ด์…˜: ์กฐ๊ธฐ `return 2` ์ œ๊ฑฐ โ†’ ํ•ด๋‹น ํ…Œ์ŠคํŠธ **FAIL** ํ™•์ธ ํ›„ ์›๋ณต - [ ] `lib_py/layout.py`: `PaneInfo.focused` ์ œ๊ฑฐ, `extract_panes_and_focus` โ†’ `extract_panes` ๊ฐœ๋ช…, `:78` ํ˜ธ์ถœ๋ถ€ ์ˆ˜์ •, `Tuple` import ์ •๋ฆฌ (ยง3.2) - [ ] `tests/test_layout.py:9,11` ๋ฏธ์‚ฌ์šฉ import ์ œ๊ฑฐ - [ ] **`lib_py/layout.py`: ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ์— `max_columns` ๊ฒ€์‚ฌ ์ถ”๊ฐ€ + ๊ทผ๊ฑฐ ์ฃผ์„ (ยง3.1) โ€” ์•„๋ž˜ env ๋ฐฐ์„ ๊ณผ ๊ฐ™์€ ์ปค๋ฐ‹** - [ ] `lib_py/layout.py`: `_env_int` ํ—ฌํผ + `--max-cols` env ๊ธฐ๋ณธ๊ฐ’ (ยง3.3). **`lib.sh` ๋Š” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ** - [ ] `tests/test_layout.py` ์— `_four_panes_two_columns()` **์ •์˜** + ์‹ ๊ทœ ํ…Œ์ŠคํŠธ **3๊ฑด** ์ถ”๊ฐ€ (ยง3.5) โ€” ํƒ€์ž… ํžŒํŠธ ๊ธˆ์ง€(M-20), `skills_dir` ์€ ํ…Œ์ŠคํŠธ ๋‚ด๋ถ€์—์„œ ๊ณ„์‚ฐ - [ ] `lib_py/layout.py`: `no_panes_default` ๋ถ„๊ธฐ ์ฃผ์„ ๋ณด๊ฐ• (ยง3.4) - [ ] `.mam.env.example` ์— `MAM_MIN_PANE_COLS` / `MAM_MIN_PANE_ROWS` / `MAM_MAX_PANE_COLS` ๋ฌธ์„œํ™” (ยง4) - [ ] `pytest tests/ -q` โ†’ **333 passed** - [ ] `pytest tests/test_deploy_freshness.py -q` โ†’ 31 passed - [ ] ยง5 ๋ฎคํ…Œ์ด์…˜ **8์ข…** ์ „๊ฑด FAIL ํ™•์ธ ํ›„ ์›๋ณต, ๋กœ๊ทธ ์ฒจ๋ถ€ - [ ] GUI/ํ—ค๋“œ๋ฆฌ์Šค๊ฐ€ `max_columns=2` + 4ํŽ˜์ธ์—์„œ **๋™์ผํ•˜๊ฒŒ** `max_columns_reached` ๋ฅผ ๋‚ด๋Š”์ง€ ์ˆ˜๋™ ํ™•์ธ - [ ] โš ๏ธ `tests/fixtures/herdr_contract.json` ์˜ `PaneInfo` ๋Š” **herdr RPC ํƒ€์ž…** โ€” ๊ฑด๋“œ๋ฆฌ์ง€ ๋ง ๊ฒƒ (M-11)