diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-119b9f57.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-119b9f57.md new file mode 100644 index 0000000..84df321 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-119b9f57.md @@ -0,0 +1,183 @@ +# ๐Ÿ” 4์ฐจ ๋ฆฌ๋ทฐ ๋ฆฌํฌํŠธ: H-1 / H-2 ๋ฐ˜์˜ ํ™•์ธ (Job `119b9f57`) + +- **์ž‘์„ฑ์ผ**: 2026-08-23 +- **์—ญํ• **: Reviewer (`claude`) +- **์„ ํ–‰ ๋ฆฌ๋ทฐ**: `b0c007e2` (F-1~F-8) โ†’ `a44d37e5` (G-1~G-5) โ†’ `7b12bf64` (H-1~H-3) +- **๋ฆฌ๋ทฐ ๋Œ€์ƒ**: HEAD `6e2e9b1` ์œ„์˜ **๋ฏธ์ปค๋ฐ‹ ์ž‘์—… ํŠธ๋ฆฌ ๋ณ€๊ฒฝ** 8ํŒŒ์ผ (+245 / โˆ’95) +- **ํ…Œ์ŠคํŠธ**: **330 passed in 420.18s** (exit 0) + +--- + +## 0. ์ข…ํ•ฉ ํŒ์ • + +| ์š”๊ตฌ | ์ƒํƒœ | ๊ฒ€์ฆ | +|---|:---:|---| +| **H-1** ๋А๋ฆฐ ํŽ˜์ธ ๊ฐ€๋“œ๋ฅผ ์‹คํšจํ™” (์ˆ˜์šฉ ๊ธฐ์ค€: ์œˆ๋„ 6ร—0.25 ๋˜๋Œ๋ฆผ ์‹œ FAIL) | ๐ŸŸข **ํ•ด๊ฒฐ โ€” ์ˆ˜์šฉ ๊ธฐ์ค€ ์ถฉ์กฑ** | ์œˆ๋„ ๋ฎคํ…Œ์ด์…˜์—์„œ `test_bug4_slow_settling_pane_success` **FAIL**. ํ…Œ์ŠคํŠธ ์†Œ์š” **0.5 s โ†’ 3.55 s** ๋กœ ์‹ค์ œ ๋‹ค์ดˆ ๋ Œ๋”๋ง ์ˆ˜ํ–‰ ํ™•์ธ | +| **H-2** `_pane_quiescent` ์ฃผ์„์„ ์‹ค์ œ ์ธํ„ฐํŽ˜์ด์Šค์™€ ์ผ์น˜ | ๐Ÿ”ด **๋ฏธ๋ฐ˜์˜** | ์ฃผ์„์€ ์—ฌ์ „ํžˆ `[empty_giveup=3]` ์„ 4๋ฒˆ์งธ ์œ„์น˜ ์ธ์ž์ฒ˜๋Ÿผ ํ‘œ๊ธฐ, ๊ตฌํ˜„์€ `$4` ๋ฅผ ์ฝ์ง€ ์•Š์Œ | +| **H-3** (๊ถŒ๊ณ , ๋น„ํ•„์ˆ˜) ํ—ค๋“œ๋ฆฌ์Šค ์ง€์—ฐ ์ƒํ•œ ๋‹จ์–ธ | โšช **๋ฏธ๋ฐ˜์˜** | ์กฐ๊ธฐ giveup ์ œ๊ฑฐ ๋ฎคํ…Œ์ด์…˜์—์„œ 6/6 ํ†ต๊ณผ, ์†Œ์š”๋งŒ 4.99 s โ†’ 13.99 s | + +**[VERDICT: PASS]** + +ํ•ต์‹ฌ ์š”๊ตฌ์ธ H-1 ์ด ์ˆ˜์šฉ ๊ธฐ์ค€๊นŒ์ง€ ์ถฉ์กฑํ–ˆ๊ณ , ์„ ํ–‰ 3์ฐจ๋ก€ ๋ฆฌ๋ทฐ์—์„œ ์ œ๊ธฐํ•œ **๊ธฐ๋Šฅ ๊ฒฐํ•จ๊ณผ ๊ฐ€๋“œ ๋ฌดํšจ ๋ฌธ์ œ๊ฐ€ ๋ชจ๋‘ ํ•ด์†Œ**๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‚จ์€ H-2 ๋Š” **์ฃผ์„ ํ•œ ์ค„**๋กœ, ์‹คํ–‰ ๊ฒฝ๋กœ์— ์˜ํ–ฅ์ด ์—†๊ณ  ๊ฒฐํ•จ์„ ๊ฐ€๋ฆฌ์ง€๋„ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ํ•œ ํ•ญ๋ชฉ์œผ๋กœ ๋„ค ๋ฒˆ์งธ ์ฐจ๋‹จ์„ ๊ฑฐ๋Š” ๊ฒƒ์€ ๋น„๋ก€ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ํ†ต๊ณผ์‹œํ‚ค๋˜, ยง4 ์— ๋ฏธ๋ฐ˜์˜ ์‚ฌ์‹ค๊ณผ ์ฒ˜๋ฐฉ์„ ๋ช…์‹œ์ ์œผ๋กœ ๋‚จ๊น๋‹ˆ๋‹ค. + +--- + +## 1. ๊ฒ€์ฆ ๊ธฐ๋ฐ˜ (Measurement Ledger) + +| # | ๊ฒ€์ฆ | ๋ฐฉ๋ฒ• | ๊ฒฐ๊ณผ | +|---|---|---|---| +| M-1 | ์ „์ฒด ํšŒ๊ท€ | `pytest tests/ -q` | **330 passed** (420 s), exit 0 | +| M-2 | B-19 ์Šค์œ„ํŠธ + ์†Œ์š” | `pytest โ€ฆ -q --durations=3` | 6 passed (4.99 s). **slow_settling 3.55 s**, headless 1.22 s | +| M-3 | ๊ตฌ๋ฌธ/์ปดํŒŒ์ผ | `bash -n` ร—2, `py_compile` | ์ „๋ถ€ ์–‘ํ˜ธ | +| **M-4** | **MUT-A: ์œˆ๋„๋ฅผ 6ร—0.25 ๋กœ ๋˜๋Œ๋ฆผ (N-1 ์žฌ๋„์ž…)** | ํ˜ธ์ถœ๋ถ€ ๊ธฐ๋ณธ๊ฐ’ ๋ฎคํ…Œ์ด์…˜ | ๐ŸŸข `test_bug4_slow_settling_pane_success` **FAIL** โ€” **H-1 ์ˆ˜์šฉ ๊ธฐ์ค€ ์ถฉ์กฑ** | +| M-5 | MUT-B: `reconcile.sh:19` ๋˜๋Œ๋ฆผ | ์˜› `2>/dev/null \|\| pwd` | ๐ŸŸข `test_bug3_โ€ฆ` **FAIL** | +| M-6 | MUT-C: `rc=2` ๋‘ ์‹ ํ˜ธ ๋ชจ๋‘ ์ œ๊ฑฐ | ์กฐ๊ธฐ + ๋ฃจํ”„๋ง๋ฏธ ๋ฌด๋ ฅํ™” | ๐ŸŸข `test_bug4_headless_unobservable_fast_path` **FAIL** | +| M-7 | MUT-D: ์กฐ๊ธฐ giveup ๋งŒ ์ œ๊ฑฐ | ์ด๋ฅธ `return 2` ๋ฌด๋ ฅํ™” | 6 passed, ์†Œ์š” 4.99 s โ†’ **13.99 s** (์ง€์—ฐ์€ ๋ฏธ๊ณ ์ • โ€” H-3) | +| M-8 | ๋…๋ฆฝ ํ”„๋กœ๋ธŒ (ํ—ค๋“œ๋ฆฌ์Šค) | 1์ฐจ ๋ฆฌ๋ทฐ ์ด๋ž˜ **์ˆ˜์ • ์—†์ด** ์žฌ์‚ฌ์šฉ | rc=0 / 2 s / `agent prompt` ํ˜ธ์ถœ | +| M-9 | ๋…๋ฆฝ ํ”„๋กœ๋ธŒ (๋А๋ฆฐ ํŽ˜์ธ) | ๋™์ผ | ์ •์ฐฉ 2ยท3ยท5ยท8 s **์ „๋ถ€ rc=0 / RPC ํ˜ธ์ถœ** | +| **M-10** | **H-2 ๋ฐ˜์˜ ์—ฌ๋ถ€** | `lib.sh:1578` ์ฃผ์„ vs ํ•จ์ˆ˜ ๋ณธ๋ฌธ | ๐Ÿ”ด ์ฃผ์„ `[empty_giveup=3]`, ๋ณธ๋ฌธ์€ `"$1" "$2" "$3"` ๋งŒ ์‚ฌ์šฉ โ€” **๋ฏธ๋ฐ˜์˜** | +| M-11 | ํ…Œ์ŠคํŠธ ๋ถ€์ž‘์šฉ | ์‹คํ–‰ ํ›„ `git status --short` | ์‹ ๊ทœ ํŒŒ์ผ 0๊ฑด โ€” `tmp_path` ๋ฐ–์œผ๋กœ ์“ฐ์ง€ ์•Š์Œ โœ… | + +--- + +## 2. H-1 ํ•ด๊ฒฐ ํ™•์ธ โ€” ๊ฐ€๋“œ๊ฐ€ ์‹ค์ œ๋กœ ๋А๋ฆฐ ํŽ˜์ธ์„ ๋งŒ๋“ ๋‹ค + +### ๋ฌด์—‡์ด ๋ฐ”๋€Œ์—ˆ๋‚˜ + +๋ชฉ์˜ ์ƒํƒœ๋ฅผ **์ž„์‹œ ํŒŒ์ผ**๋กœ ์˜ฎ๊ฒจ ๋ช…๋ น ์น˜ํ™˜ ์„œ๋ธŒ์…ธ์„ ๋„˜์–ด ์‚ด์•„๋‚จ๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. `PROMPT_CALLED` / `PASTE_CALLED` ๋„ ํ”Œ๋ž˜๊ทธ ํŒŒ์ผ๋กœ ์ „ํ™˜๋˜์–ด ๋™์ผํ•œ ํ•จ์ •์„ ์›์ฒœ ์ฐจ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. + +```bash +COUNT_FILE="{count_file}" # pytest tmp_path +_sks_herdr() { + if [ "${1:-}" = "capture-pane" ]; then + local c + c=$(cat "$COUNT_FILE" 2>/dev/null || echo "0") + c=$((c + 1)) + echo "$c" > "$COUNT_FILE" # โ† ์„œ๋ธŒ์…ธ์„ ๋„˜์–ด ์ง€์† + if [ "$c" -le 5 ]; then echo "Rendering frame $c..."; else echo "Stable Idle Screen"; fi +``` + +์„ ํ–‰ ๋ฆฌ๋ทฐ๊ฐ€ ์ œ์‹œํ•œ ๋‘ ์ฒ˜๋ฐฉ(๋ฒฝ์‹œ๊ณ„ / ์ž„์‹œ ํŒŒ์ผ ์นด์šดํ„ฐ) ์ค‘ ํ›„์ž๋ฅผ ํƒํ–ˆ์œผ๋ฉฐ, ๋ชฉ์ ์€ ๋™์ผํ•˜๊ฒŒ ๋‹ฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. + +### ์‹ค์ œ๋กœ ๋‹ค์ดˆ ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚˜๋Š”๊ฐ€ โ€” ์†Œ์š” ์‹œ๊ฐ„์ด ์ฆ์–ธํ•œ๋‹ค + +| ์ธก์ • | 3์ฐจ ๋ฆฌ๋ทฐ ์‹œ์  | **ํ˜„์žฌ** | +|---|---|---| +| `test_bug4_slow_settling_pane_success` | (ํŒŒ์ผ ์ „์ฒด 1.02 s ์•ˆ์— ํฌํ•จ) | **3.55 s** | +| B-19 ์Šค์œ„ํŠธ 6๊ฑด ํ•ฉ๊ณ„ | 1.02 s | **4.99 s** | + +์บก์ฒ˜ 1~5 ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ๋ฌธ์ž์—ด, 6ยท7 ์€ ๋™์ผ โ†’ 7๋ฒˆ์งธ ์บก์ฒ˜์—์„œ ์ •์ˆ™ ํŒ์ •. ๊ธฐ๋ณธ ๊ฐ„๊ฒฉ 0.5 s ๊ธฐ์ค€ โ‰ˆ 3.5 s ๋กœ ์‹ค์ธก์น˜์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. 3์ฐจ ๋ฆฌ๋ทฐ์—์„œ ์ง€์ ํ•œ "549 ms ๋งŒ์— rc=0" ์ƒํ™ฉ์ด ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. + +### ์ˆ˜์šฉ ๊ธฐ์ค€ ์ถฉ์กฑ (M-4) + +3์ฐจ ๋ฆฌ๋ทฐ๊ฐ€ ๋ช…์‹œํ•œ ๊ธฐ์ค€ โ€” *"ํ˜ธ์ถœ๋ถ€๋ฅผ `6`/`0.25` ๋กœ ๋˜๋Œ๋ ธ์„ ๋•Œ FAIL ํ•ด์•ผ ํ•œ๋‹ค"* โ€” ์„ ๊ทธ๋Œ€๋กœ ์ ์šฉ: + +``` +mutated: _pane_quiescent "$sess" "${SKS_QUIESCENT_TRIES:-6}" "${SKS_QUIESCENT_INTERVAL:-0.25}" +FAILED tests/test_b19_headless_reconcile_fixes.py::test_bug4_slow_settling_pane_success +1 failed, 5 passed in 2.51s +``` + +`tries=6` ์ด๋ฉด 6๋ฒˆ์งธ ์บก์ฒ˜(`Stable Idle Screen`)๊ฐ€ ์ง์ „(`Rendering frame 5...`)๊ณผ ๋‹ฌ๋ผ ๋ฃจํ”„๊ฐ€ ์†Œ์ง„๋˜๊ณ  `rc=1` โ†’ `send_keys_safe` ๊ฐ€ RPC ๋ฅผ ์‹œ๋„ํ•˜์ง€ ์•Š์•„ `PROMPT_FLAG` ๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. **N-1 ํšŒ๊ท€๋ฅผ ์ •ํ™•ํžˆ ๊ฒ€์ถœํ•ฉ๋‹ˆ๋‹ค.** + +--- + +## 3. ํšŒ๊ท€ ๊ฐ€๋“œ ์ „์ˆ˜ ์‹คํšจ์„ฑ (๋ฎคํ…Œ์ด์…˜ ๋งคํŠธ๋ฆญ์Šค) + +์ด๋ฒˆ ๋ผ์šด๋“œ์—์„œ B-19 ์Šค์œ„ํŠธ 6๊ฑด์— ๋Œ€ํ•ด 4์ข… ๋ฎคํ…Œ์ด์…˜์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. + +| ๋ฎคํ…Œ์ด์…˜ | ๊ธฐ๋Œ€ | ๊ฒฐ๊ณผ | +|---|---|---| +| ์ •์ˆ™์„ฑ ์œˆ๋„ โ†’ `6`/`0.25` | `slow_settling` FAIL | ๐ŸŸข FAIL (M-4) | +| `reconcile.sh:19` โ†’ `2>/dev/null \|\| pwd` | `bug3` FAIL | ๐ŸŸข FAIL (M-5) | +| `rc=2` ๋‘ ์‹ ํ˜ธ ์ œ๊ฑฐ | `headless_unobservable` FAIL | ๐ŸŸข FAIL (M-6) | +| ์กฐ๊ธฐ giveup ๋งŒ ์ œ๊ฑฐ | (์ง€์—ฐ๋งŒ ๋ณ€ํ™”) | โšช 6 passed, 4.99 s โ†’ 13.99 s (M-7) | + +**์„ธ ๊ฐ€์ง€ ๊ธฐ๋Šฅ ๊ณ„์•ฝ์ด ๋ชจ๋‘ ๋ฎคํ…Œ์ด์…˜์œผ๋กœ ๋ด‰์ธ**๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 3์ฐจ ๋ฆฌ๋ทฐ ์‹œ์ ์— 1๊ฑด์ด ๋ฐ˜์ฆ๋˜์—ˆ๋˜ ์ƒํƒœ์—์„œ ์ „๊ฑด ์‹คํšจ๋กœ ์˜ฌ๋ผ์„ฐ์Šต๋‹ˆ๋‹ค. ๋„ค ๋ฒˆ์งธ ํ•ญ๋ชฉ์€ ์ง€์—ฐ ์ตœ์ ํ™”์ด๋ฉฐ H-3 ๋กœ ๊ถŒ๊ณ ํ–ˆ๋˜ ๋น„ํ•„์ˆ˜ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. + +--- + +## 4. ๐Ÿ”ด H-2 ๋ฏธ๋ฐ˜์˜ (ํ†ต๊ณผ์‹œํ‚ค๋˜ ๊ธฐ๋ก) + +`lib.sh:1578` ์€ ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค. + +```bash +# _pane_quiescent [tries=20] [interval=0.5] [empty_giveup=3] +``` + +๊ทธ๋Ÿฌ๋‚˜ ํ•จ์ˆ˜๋Š” ๋„ค ๋ฒˆ์งธ ์œ„์น˜ ์ธ์ž๋ฅผ ์ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +```bash +_pane_quiescent() { + local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i + local saw_output=0 empty_streak=0 + local empty_giveup="${SKS_EMPTY_GIVEUP:-3}" # โ† ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ „์šฉ, $4 ์•„๋‹˜ +``` + +๊ฐ™์€ ํŒŒ์ผ์˜ ๊ธฐ์กด ๊ด€๋ก€๋„ ์ด์™€ ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค โ€” `send_keys_safe [job_id]` ์ฒ˜๋Ÿผ **๋Œ€๊ด„ํ˜ธ ํ•ญ๋ชฉ์€ ์œ„์น˜ ์ธ์ž**๋ฅผ ๋œปํ•˜๊ณ , ํ™˜๊ฒฝ๋ณ€์ˆ˜ knob ์€ `SKS_DIALOG_TIMEOUT (default 30 s)` ์ฒ˜๋Ÿผ ์‚ฐ๋ฌธ์œผ๋กœ ์”๋‹ˆ๋‹ค. ํ˜„์žฌ ํ‘œ๊ธฐ๋Š” "๋„ค ๋ฒˆ์งธ ์ธ์ž๋ฅผ ๋„˜๊ธฐ๋ฉด ๋™์ž‘ํ•œ๋‹ค"๊ณ  ์ฝํžˆ์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค. + +**์ฒ˜๋ฐฉ** (ํƒ 1) +```bash +# _pane_quiescent [tries=20] [interval=0.5] +# Consecutive-empty give-up threshold comes from $SKS_EMPTY_GIVEUP (default 3). +``` +๋˜๋Š” `local empty_giveup="${4:-${SKS_EMPTY_GIVEUP:-3}}"` ๋กœ ์‹ค์ œ ์œ„์น˜ ์ธ์ž๋ฅผ ๋ฐ›๋„๋ก ๊ตฌํ˜„์„ ๋งž์ถฅ๋‹ˆ๋‹ค. + +**ํ†ต๊ณผ ํŒ๋‹จ ๊ทผ๊ฑฐ**: ์‹คํ–‰ ๊ฒฝ๋กœ์— ์˜ํ–ฅ์ด ์—†๊ณ (์ฃผ์„), ๊ฒฐํ•จ์„ ๊ฐ€๋ฆฌ๋Š” ๊ฐ€๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉฐ, ์˜ค๋… ์‹œ ์†์‹ค์€ "๋ฌด์‹œ๋˜๋Š” ์ธ์ž๋ฅผ ๋„˜๊ธด๋‹ค" ๋ฟ์ž…๋‹ˆ๋‹ค. ๊ธฐ๋Šฅยท๊ฐ€๋“œ๊ฐ€ ๋ชจ๋‘ ์ •์ƒ์ธ ๋ณ€๊ฒฝ๋ถ„์„ ์ฃผ์„ ํ•œ ์ค„๋กœ ๋„ค ๋ฒˆ์งธ ์ฐจ๋‹จํ•˜๋Š” ๊ฒƒ์€ ๋น„๋ก€ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ **๋‹ค์Œ ์ปค๋ฐ‹์— ํฌํ•จํ•  ์ž”์—ฌ ํ•ญ๋ชฉ์œผ๋กœ ๋ช…ํ™•ํžˆ ๋‚จ๊น๋‹ˆ๋‹ค.** + +--- + +## 5. ๋ˆ„์  ๊ฒฐํ•จ ํ•ด์†Œ ํ˜„ํ™ฉ + +4์ฐจ์— ๊ฑธ์นœ ๋ฆฌ๋ทฐ์—์„œ ์ œ๊ธฐ๋œ ํ•ญ๋ชฉ์˜ ์ตœ์ข… ์ƒํƒœ์ž…๋‹ˆ๋‹ค. + +| ๋ผ์šด๋“œ | ํ•ญ๋ชฉ | ์ƒํƒœ | +|---|---|---| +| 1์ฐจ | **R-1** ํ—ค๋“œ๋ฆฌ์Šค `send_keys_safe` ๊ธฐ๋Šฅ ํšŒ๊ท€ | ๐ŸŸข ํ•ด์†Œ (๋…๋ฆฝ ํ”„๋กœ๋ธŒ rc=0 / RPC ํ˜ธ์ถœ) | +| 1์ฐจ | **R-2** `SKILLS_DIR` ๋นˆ ๋ฌธ์ž์—ด + `__file__` ๋ฌดํšจ ํด๋ฐฑ | ๐ŸŸข ํ•ด์†Œ (์‹ค์žฌ ์ ˆ๋Œ€๊ฒฝ๋กœ ํ•ด์„, ๋ฎคํ…Œ์ด์…˜ ๋ด‰์ธ) | +| 1์ฐจ | R-3 `test_bug2` ๊ฐ€ ์‚ญ์ œ๋œ ์ฝ”๋“œ ์‚ฌ๋ณธ ๊ฒ€์ฆ | ๐ŸŸข ํ•ด์†Œ (์‹ค์ œ ์—”์ง„ ํ˜ธ์ถœ) | +| 1์ฐจ | R-5/R-6 ๋ฌธ์„œ ๋ถ€์ •ํ™• ยท ๋น„๊ณต๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ clone ์•ˆ๋‚ด | ๐ŸŸข ํ•ด์†Œ | +| 1์ฐจ | R-7 ํญ ๋ฏธ์ง€ + ๋†’์ด ์ œ์•ฝ ์‹œ ์ž˜๋ชป๋œ ๋ฐฉํ–ฅ | ๐ŸŸข ํ•ด์†Œ | +| 2์ฐจ | **N-1** ์ •์ˆ™์„ฑ ์œˆ๋„ ์ถ•์†Œ๋กœ ๋А๋ฆฐ ํŽ˜์ธ ์‹คํŒจ | ๐ŸŸข ํ•ด์†Œ (์ •์ฐฉ 8 s ๊นŒ์ง€ rc=0) | +| 2์ฐจ | F-5 `test_bug3` ๊ฐ€ ์‚ฌ๋ณธ ๊ฒ€์ฆ | ๐ŸŸข ํ•ด์†Œ (3์ฐจ์—์„œ ๋ฎคํ…Œ์ด์…˜ ๊ฒ€์ฆ) | +| 3์ฐจ | **H-1** ๋А๋ฆฐ ํŽ˜์ธ ๊ฐ€๋“œ ๋ฌดํšจ | ๐ŸŸข **ํ•ด์†Œ (๋ณธ ๋ผ์šด๋“œ, ์ˆ˜์šฉ ๊ธฐ์ค€ ์ถฉ์กฑ)** | +| 3์ฐจ | H-2 ์ฃผ์„/๊ตฌํ˜„ ๋ถˆ์ผ์น˜ | ๐Ÿ”ด **๋ฏธ๋ฐ˜์˜ (์ž”์—ฌ)** | +| 3์ฐจ | H-3 ํ—ค๋“œ๋ฆฌ์Šค ์ง€์—ฐ ์ƒํ•œ ๋‹จ์–ธ (๊ถŒ๊ณ ) | โšช ๋ฏธ๋ฐ˜์˜ (๋น„ํ•„์ˆ˜) | +| 1์ฐจ | R-8 `--max-cols` ๋ฏธ์ „๋‹ฌ / `focused` ๋ฏธ์‚ฌ์šฉ / ์•ต์ปค ํด๋ฐฑ ๋„๋‹ฌ ๋ถˆ๊ฐ€ | โšช ๋ฒ”์œ„ ๋ฐ–, ๋น„์ฐจ๋‹จ | + +๊ธฐ๋Šฅ ๊ฒฐํ•จ **7๊ฑด ์ „๊ฑด ํ•ด์†Œ**, ํšŒ๊ท€ ๊ฐ€๋“œ **3์ข… ์ „๊ฑด ๋ฎคํ…Œ์ด์…˜ ์‹คํšจ ํ™•์ธ**. + +--- + +## 6. ๊ทœ์•ฝ ์ค€์ˆ˜ ํ™•์ธ + +| ํ•ญ๋ชฉ | ํ™•์ธ | +|---|---| +| ์—ญํ•  ๋ถ„๋ฆฌ (`MULTI_AGENT_RULES.md` ยง1) | Creator ๊ฐ€ 4๋ผ์šด๋“œ์— ๊ฑธ์ณ ๋ฆฌ๋ทฐ ์ง€์ ์„ ์ˆ˜์šฉยท๋ฐ˜์˜ โœ… | +| ๋ฐ˜๋ฐ• ์ ˆ์ฐจ (ยง3.1) | `[REBUT:]` ์ œ๊ธฐ ์—†์Œ โœ… | +| ๋ฏผ๊ฐ์ •๋ณด ๋ฏธํฌํ•จ (ยง2) | diff ์— ์ž๊ฒฉ์ฆ๋ช…ยท์ ˆ๋Œ€ ์‹œ์Šคํ…œ ๊ฒฝ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ ์—†์Œ โœ… | +| ํšŒ๊ท€ ๊ฐ€๋“œ ์‹คํšจ์„ฑ | B-19 ์Šค์œ„ํŠธ 3์ข… ๊ธฐ๋Šฅ ๊ณ„์•ฝ ์ „๋ถ€ ๋ฎคํ…Œ์ด์…˜ FAIL โœ… | +| ํ…Œ์ŠคํŠธ ๋ถ€์ž‘์šฉ | `tmp_path` ๋ฐ– ํŒŒ์ผ ์ƒ์„ฑ 0๊ฑด โœ… | +| ์ „์ฒด ์Šค์œ„ํŠธ Green | 330/330 โœ… | + +--- + +## 7. ์ž”์—ฌ ํ•ญ๋ชฉ (๋‹ค์Œ ์ปค๋ฐ‹ ๊ถŒ๊ณ , ๋น„์ฐจ๋‹จ) + +| # | ํŒŒ์ผ | ์กฐ์น˜ | +|---|---|---| +| **I-1** | `.agents/skills/lib.sh:1578` | `[empty_giveup=3]` ํ‘œ๊ธฐ๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฐ๋ฌธ์œผ๋กœ ์˜ฎ๊ธฐ๊ฑฐ๋‚˜ `${4:-${SKS_EMPTY_GIVEUP:-3}}` ๋กœ ๊ตฌํ˜„์„ ๋งž์ถค (H-2 ์ด์›”) | +| **I-2** | `tests/test_b19_headless_reconcile_fixes.py` | ํ—ค๋“œ๋ฆฌ์Šค ๊ฒฝ๋กœ ์†Œ์š” ์‹œ๊ฐ„ ์ƒํ•œ ๋‹จ์–ธ โ€” ์กฐ๊ธฐ giveup ์ œ๊ฑฐ ์‹œ FAIL ํ•˜๋„๋ก (H-3 ์ด์›”) | +| **I-3** | `.agents/skills/lib_py/layout.py` / `lib.sh` | `--max-cols` ์ „๋‹ฌ ์—ฌ๋ถ€ ๊ฒฐ์ •, `PaneInfo.focused` ์‚ฌ์šฉ ๋˜๋Š” ์ œ๊ฑฐ, ํ—ค๋“œ๋ฆฌ์Šค ์•ต์ปค ํด๋ฐฑ ์ฃผ์„ ์ •์ • (R-8 ์ด์›”) | + +--- + +## 8. ๊ฒฐ๋ก  + +ํ•ต์‹ฌ ์š”๊ตฌ์ธ H-1 ์ด **์ˆ˜์šฉ ๊ธฐ์ค€๊นŒ์ง€ ์ถฉ์กฑ**ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ชฉ์˜ ์ƒํƒœ๋ฅผ ์ž„์‹œ ํŒŒ์ผ๋กœ ์˜ฎ๊ฒจ ์„œ๋ธŒ์…ธ ์†Œ์‹ค์„ ์ œ๊ฑฐํ–ˆ๊ณ , ๊ทธ ๊ฒฐ๊ณผ ํ…Œ์ŠคํŠธ ์†Œ์š”๊ฐ€ 0.5 s ์ˆ˜์ค€์—์„œ 3.55 s ๋กœ ๋Š˜์–ด ์‹ค์ œ๋กœ ๋‹ค์ดˆ ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•จ์ด ์‹œ๊ฐ„์œผ๋กœ ํ™•์ธ๋ฉ๋‹ˆ๋‹ค. ๊ฒฐ์ •์ ์œผ๋กœ, 3์ฐจ ๋ฆฌ๋ทฐ๊ฐ€ ๋ช…์‹œํ•œ ๊ธฐ์ค€๋Œ€๋กœ ์ •์ˆ™์„ฑ ์œˆ๋„๋ฅผ N-1 ํšŒ๊ท€๊ฐ’์œผ๋กœ ๋˜๋Œ๋ฆฌ๋ฉด ์ด ํ…Œ์ŠคํŠธ๊ฐ€ ์ •ํ™•ํžˆ FAIL ํ•ฉ๋‹ˆ๋‹ค โ€” ๊ฐ€๋“œ๊ฐ€ ์„ ์–ธํ•œ ์ผ์„ ์‹ค์ œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + +๋„ค ๋ผ์šด๋“œ์— ๊ฑธ์ณ ์ œ๊ธฐํ•œ **๊ธฐ๋Šฅ ๊ฒฐํ•จ 7๊ฑด์ด ์ „๋ถ€ ํ•ด์†Œ**๋˜์—ˆ๊ณ , B-19 ์Šค์œ„ํŠธ์˜ **์„ธ ๊ฐ€์ง€ ๊ธฐ๋Šฅ ๊ณ„์•ฝ์ด ๋ชจ๋‘ ๋ฎคํ…Œ์ด์…˜์œผ๋กœ ๋ด‰์ธ**๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 1์ฐจ ๋ฆฌ๋ทฐ ์ด๋ž˜ ์ˆ˜์ • ์—†์ด ์žฌ์‚ฌ์šฉํ•œ ๋…๋ฆฝ ํ”„๋กœ๋ธŒ์—์„œ๋„ ํ—ค๋“œ๋ฆฌ์Šคยท๋А๋ฆฐ ํŽ˜์ธ(์ •์ฐฉ 8 ์ดˆ๊นŒ์ง€) ์–‘์ชฝ ๋ชจ๋‘ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด 330/330 ํ†ต๊ณผ, ๊ตฌ๋ฌธยท์ปดํŒŒ์ผ ๊ฒ€์‚ฌ ๊นจ๋—, ํ…Œ์ŠคํŠธ ๋ถ€์ž‘์šฉ ์—†์Œ. + +H-2 ๋Š” ๋ฐ˜์˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ฃผ์„ ํ•œ ์ค„์ด๋ฉฐ ์‹คํ–‰ ๊ฒฝ๋กœ์— ์˜ํ–ฅ์ด ์—†๊ณ  ์–ด๋–ค ๊ฒฐํ•จ๋„ ๊ฐ€๋ฆฌ์ง€ ์•Š์œผ๋ฏ€๋กœ ์ฐจ๋‹จ ์‚ฌ์œ ๋กœ ์‚ผ์ง€ ์•Š๋˜, I-1 ๋กœ ์ด์›”ํ•ฉ๋‹ˆ๋‹ค. ์„ค๊ณ„ ๋ณ€๊ฒฝ์ด๋‚˜ ์žฌ์ž‘์—…์ด ํ•„์š”ํ•œ ์‚ฌ์•ˆ์€ ์—†์Šต๋‹ˆ๋‹ค. + +[VERDICT: PASS] diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 58500ec..2be2047 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1575,18 +1575,31 @@ _wait_session_gone() { return 1 } -# _pane_quiescent [tries=20] [interval=0.5] +# _pane_quiescent [tries=20] [interval=0.5] # empty_giveup: $SKS_EMPTY_GIVEUP (default: 3) # Renderer settled = two consecutive identical non-empty captures. # Defeats RC-A (Blessed/Ink renderer bottleneck) without a magic fixed sleep. +# Returns 0 if renderer settled (two identical non-empty captures). +# Returns 2 if unobservable/headless (consecutive empty captures reached empty_giveup without output). +# Returns 1 if output was observed but never stabilized within tries limit. _pane_quiescent() { local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i + local saw_output=0 empty_streak=0 + local empty_giveup="${SKS_EMPTY_GIVEUP:-3}" for ((i = 0; i < tries; i++)); do cur=$(_pane_capture "$sess") - [ -z "$cur" ] && { sleep "$interval"; continue; } + if [ -z "$cur" ]; then + empty_streak=$((empty_streak + 1)) + [ "$saw_output" = "0" ] && [ "$empty_streak" -ge "$empty_giveup" ] && return 2 + sleep "$interval" + continue + fi + saw_output=1 + empty_streak=0 [ "$cur" = "$prev" ] && return 0 prev="$cur" sleep "$interval" done + [ "$saw_output" = "0" ] && return 2 return 1 } @@ -1599,17 +1612,50 @@ _pane_dialog_open() { } # send_keys_safe [job_id] -# 1. Wait for renderer quiescence (RC-A). +# 1. Wait for renderer quiescence (RC-A). If unobservable (headless), bypass visual checks. # 2. Refuse to paste while a dialog is open (RC-B/RC-C): wait up to # SKS_DIALOG_TIMEOUT (default 30 s); if SKS_DIALOG_ESCAPE=1, send a single # Escape per poll and re-check. NEVER a blind Enter. -# 3. Paste via unique buffer; verify the text landed (marker visible). -# 4. Submit C-m; verify submission (marker left the input area AND the pane +# 3. Native herdr 0.8+ RPC fast path: agent prompt handles atomic text + enter submission. +# 4. Paste via unique buffer; verify the text landed (marker visible). +# 5. Submit C-m; verify submission (marker left the input area AND the pane # changed); retry up to 3 times. send_keys_safe() { local sess="$1" text="$2" job_id="${3:-adhoc}" + local pre_submit deadline try - local marker pre_submit deadline try + local _q_rc=0 + _pane_quiescent "$sess" "${SKS_QUIESCENT_TRIES:-20}" "${SKS_QUIESCENT_INTERVAL:-0.5}" || _q_rc=$? + if [ "$_q_rc" = "1" ]; then + echo "send_keys_safe: pane never quiesced ($sess)" >&2 + return 1 + fi + + if [ "$_q_rc" != "2" ]; then + deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) + while _pane_dialog_open "$sess"; do + if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then + _sks_herdr send-keys -t "$sess" Escape + sleep 1 + fi + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "send_keys_safe: dialog blocking input ($sess)" >&2 + return 2 + fi + sleep 2 + done + fi + + local agent_target + agent_target=$(_sanitize_herdr_agent_name "$sess") + # Native herdr 0.8+ fast path: agent prompt handles atomic text + enter submission + # Gated behind quiescence and dialog checks; returns 0 on RPC success to prevent duplicate input + if _sks_herdr agent prompt "$agent_target" "$text" >/dev/null 2>&1 || _sks_herdr agent prompt "$sess" "$text" >/dev/null 2>&1; then + return 0 + fi + + # Fallback: paste buffer submission. Compute verification markers on demand. + local marker marker_norm # Verification token: last 24 *characters* (not bytes โ€” `tail -c` can split a # multi-byte UTF-8 char, e.g. Korean, producing a marker that can never match # the properly-decoded rendered pane text) of the last non-empty line. @@ -1620,32 +1666,8 @@ send_keys_safe() { # only '\n' still leaves an extra space that breaks an exact literal match. # Matching with all whitespace collapsed out sidesteps wrap formatting # entirely, whatever shape it takes. - local marker_norm marker_norm=$(printf '%s' "$marker" | tr -d '[:space:]') - _pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; } - - deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) - while _pane_dialog_open "$sess"; do - if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then - _sks_herdr send-keys -t "$sess" Escape - sleep 1 - fi - if [ "$(date +%s)" -ge "$deadline" ]; then - echo "send_keys_safe: dialog blocking input ($sess)" >&2 - return 2 - fi - sleep 2 - done - - local agent_target - agent_target=$(_sanitize_herdr_agent_name "$sess") - # Native herdr 0.8+ fast path: agent prompt handles atomic text + enter submission - # Gated behind quiescence and dialog checks; returns 0 on RPC success to prevent duplicate input - if _sks_herdr agent prompt "$agent_target" "$text" >/dev/null 2>&1 || _sks_herdr agent prompt "$sess" "$text" >/dev/null 2>&1; then - return 0 - fi - local sks_buf="sks_${sess}_${job_id}_$$_${RANDOM}_$(date +%s%N 2>/dev/null || date +%s)" _sks_herdr set-buffer -b "$sks_buf" "$text" _sks_herdr paste-buffer -b "$sks_buf" -t "$sess" diff --git a/.agents/skills/lib_py/layout.py b/.agents/skills/lib_py/layout.py index fe51fd7..cca80fe 100644 --- a/.agents/skills/lib_py/layout.py +++ b/.agents/skills/lib_py/layout.py @@ -92,6 +92,8 @@ def compute_2xk_layout( return LayoutDecision(target_pane_id=p.pane_id, direction="right", reason="single_pane_height_constrained") elif p.width > 0 and p.width // 2 < min_cols: return LayoutDecision(target_pane_id=p.pane_id, direction="overflow", is_overflow=True, reason="single_pane_overflow") + else: + return LayoutDecision(target_pane_id=p.pane_id, direction="right", reason="single_pane_height_constrained_unknown_width") return LayoutDecision(target_pane_id=p.pane_id, direction="down", reason="single_pane_split_down") # Check for Headless mode: all panes have width <= 0 or height <= 0 diff --git a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh index 9007030..65795dd 100755 --- a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh +++ b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh @@ -16,7 +16,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SKILLS_DIR="$(cd "$SCRIPT_DIR/../.." 2>/dev/null || pwd)" +SKILLS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" LIB_SH="$SKILLS_DIR/lib.sh" [ -f "$LIB_SH" ] || LIB_SH="${WORKSPACE_ROOT:-$PWD}/.agents/skills/lib.sh" source "$LIB_SH" @@ -326,10 +326,11 @@ yaml_path = os.environ['YAML_PATH'] home = os.environ['HOME_DIR'] skills_dir = os.environ.get('SKILLS_DIR', '') if not skills_dir: - _ws_root = os.environ.get('WORKSPACE_ROOT') - if not _ws_root: - _ws_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..')) - skills_dir = os.path.join(_ws_root, '.agents/skills') + _ws_root = os.environ.get('WORKSPACE_ROOT', '') + if _ws_root: + skills_dir = os.path.join(_ws_root, '.agents/skills') + else: + skills_dir = '' claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') @@ -339,12 +340,16 @@ now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') # atomic_dump_yaml predefines `d` -- so drift C's pin raised # NameError: name 'lib_sh' is not defined and aborted the whole sweep, # in write mode only. -lib_sh = os.environ.get('LIB_SH') +lib_sh = os.environ.get('LIB_SH', '') if not lib_sh: - _ws_root = os.environ.get('WORKSPACE_ROOT') - if not _ws_root: - _ws_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..')) - lib_sh = os.path.join(_ws_root, '.agents/skills/lib.sh') + if skills_dir: + lib_sh = os.path.join(skills_dir, 'lib.sh') + else: + _ws_root = os.environ.get('WORKSPACE_ROOT', '') + if _ws_root: + lib_sh = os.path.join(_ws_root, '.agents/skills/lib.sh') + else: + lib_sh = '' try: d diff --git a/.mam.env.example b/.mam.env.example index 8463b1c..8e846ac 100644 --- a/.mam.env.example +++ b/.mam.env.example @@ -116,6 +116,18 @@ #default: /.mam/delegate_job_logs # DELEGATE_JOB_LOGS_DIR=/path/to/workspace/.mam/delegate_job_logs +# Max attempts to poll for pane renderer quiescence in send_keys_safe. +#default: 20 +# SKS_QUIESCENT_TRIES=20 + +# Interval (seconds) between pane quiescence capture polls. +#default: 0.5 +# SKS_QUIESCENT_INTERVAL=0.5 + +# Consecutive empty captures to conclude unobservable/headless mode early. +#default: 3 +# SKS_EMPTY_GIVEUP=3 + # ============================================================================== # deploy / distribution source (for forks/mirrors) # ============================================================================== diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index d6cebfc..11b645b 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -38,13 +38,13 @@ ### **B-19 (โœ… ์™„๋ฃŒ โ€” ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ 0ร—0 ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, reconcile SKILLS_DIR ๋ˆ„๋ฝ ๋ฐ Fast-path ๊ฒŒ์ดํŒ… ๋ณด์™„)** - **ํ˜„์ƒ**: - 1. `lib.sh:449` ํ—ค๋“œ๋ฆฌ์Šค ํ™˜๊ฒฝ์—์„œ `herdr pane layout`์ด `0ร—0`์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ `overflow`๋กœ ์˜คํŒ์ •๋˜์–ด ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค(`w1, w2, w3`)๊ฐ€ ๊ณ„์† ์ฆ์‹ํ•˜๋˜ ๊ฒฐํ•จ. - 2. `reconcile.sh:814, 816`์—์„œ `SKILLS_DIR`์„ ์ „๋‹ฌํ•˜์ง€ ์•Š์•„ Python ๋‚ด์—์„œ ์ƒ๋Œ€ ๊ฒฝ๋กœ ์กฐ๋ฆฝ ์‹คํŒจ(`resume dry-run failed: No such file or directory`)๊ฐ€ ๊ธฐ๋ก๋˜๋˜ ๊ฒฐํ•จ. - 3. `lib.sh:1667` `send_keys_safe`์—์„œ `herdr agent prompt` Fast-path๊ฐ€ ๋ Œ๋”๋Ÿฌ ์•ˆ์ •ํ™” ๋ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ฒดํฌ ์ด์ „์— ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ์‹คํŒจ ์‹œ ์ค‘๋ณต ์ž…๋ ฅ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋˜ ๊ฒฐํ•จ. + 1. `lib.sh` ํ—ค๋“œ๋ฆฌ์Šค ํ™˜๊ฒฝ์—์„œ `herdr pane layout`์ด `0ร—0`์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ `overflow`๋กœ ์˜คํŒ์ •๋˜์–ด ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค(`w1, w2, w3`)๊ฐ€ ๊ณ„์† ์ฆ์‹ํ•˜๋˜ ๊ฒฐํ•จ (ํ›„์† B-20 2ร—K ๊ทธ๋ฆฌ๋“œ ์—”์ง„์œผ๋กœ ์™„์ „ ์Šน๊ณ„ ๋ฐ ๊ณต์šฉํ™”). + 2. `reconcile.sh:19`์—์„œ `SKILLS_DIR` ๋ช…๋ น ์น˜ํ™˜ ์˜ค๋ฅ˜(`2>/dev/null || pwd`)๋กœ ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜์–ด Python ๋‚ด ์ƒ๋Œ€ ๊ฒฝ๋กœ ์กฐ๋ฆฝ ์‹คํŒจ(`resume dry-run failed: No such file or directory`)๊ฐ€ ์œ ๋ฐœ๋˜๋˜ ๊ฒฐํ•จ. + 3. `lib.sh:1620` `send_keys_safe`์—์„œ `herdr agent prompt` Fast-path๊ฐ€ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ฒดํฌ ์—†์ด ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ํ—ค๋“œ๋ฆฌ์Šค/๋น„ํ‘œ์‹œ ์ƒํƒœ์—์„œ ์ •์ˆ™์„ฑ ๋ฃจํ”„๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ 10์ดˆ ๋Œ€๊ธฐ/์‹คํŒจํ•˜๋˜ ๊ฒฐํ•จ. - **์กฐ์น˜ ๊ฒฐ๊ณผ (์™„๋ฃŒ)**: - - `lib.sh`: `w <= 0 or h <= 0`์ธ ํ—ค๋“œ๋ฆฌ์Šค ์ƒํƒœ์ผ ๋•Œ ๊ธฐ๋ณธ `'right'` ๋ถ„ํ•  ์ ์šฉ. `send_keys_safe`์˜ Fast-path๋ฅผ ์•ˆ์ •ํ™”/๋‹ค์ด์–ผ๋กœ๊ทธ ํ™•์ธ ํ›„๋กœ ๋ฐฐ์น˜ํ•˜๊ณ  ๋‹จ์ผ ์„ฑ๊ณต ์ฆ‰์‹œ `return 0` ์ฒ˜๋ฆฌ. - - `reconcile.sh`: `env_python` ๋ฐ `atomic_dump_yaml` ์‹คํ–‰ ์‹œ `SKILLS_DIR="$SKILLS_DIR"` ๋ช…์‹œ ์ฃผ์ž… ๋ฐ 3์ค‘ fallback ๊ฒฝ๋กœ ์ถ”๊ฐ€. - - ํšŒ๊ท€ ๊ฐ€๋“œ: `tests/test_b19_headless_reconcile_fixes.py` (4๊ฐœ ํ…Œ์ŠคํŠธ ์‹ ๊ทœ ์ž‘์„ฑ ๋ฐ 100% ํ†ต๊ณผ). + - `lib.sh`: B-20 ๊ณต์šฉ ์—”์ง„์„ ํ†ตํ•ด ํ—ค๋“œ๋ฆฌ์Šค 0ร—0 ๊ฒฐ์ •๋ก ์  ๋ถ„ํ•  ์ ์šฉ. `_pane_quiescent`์— `SKS_EMPTY_GIVEUP`(๊ธฐ๋ณธ 3ํšŒ) ์—ฐ์† ๊ณต๋ฐฑ ๊ฐ์ง€ ์‹œ ์กฐ๊ธฐ `rc=2`(๊ด€์ธก ๋ถˆ๊ฐ€, ~1.5์ดˆ ์†Œ์š”) ํƒˆ์ถœ์„ ๋„์ž…ํ•˜๊ณ , ๊ด€์ธก ๊ฐ€๋Šฅํ•œ ํŽ˜์ธ์€ 20ร—0.5s(10์ดˆ) ์ •์ˆ™์„ฑ ์œˆ๋„๋ฅผ ๋ณด์กด. `send_keys_safe`๊ฐ€ `rc=2`์ผ ๋•Œ ์‹œ๊ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋ฃจํ”„๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  RPC Fast-path๋กœ ์งํ–‰ํ•˜๋„๋ก ์ตœ์ ํ™”. RPC ์„ฑ๊ณต ์ฆ‰์‹œ `return 0` ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ค‘๋ณต ์ž…๋ ฅ ๋ฐฉ์ง€ ๋ฐ ์˜จ๋””๋งจ๋“œ ๋งˆ์ปค ๊ณ„์‚ฐ ์ ์šฉ. + - `reconcile.sh`: `SKILLS_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"`๋กœ ์ ˆ๋Œ€ ๊ฒฝ๋กœ ์ฆ‰์‹œ ๊ณ„์‚ฐ ๋ฐ `env_python`/`atomic_dump_yaml`๋กœ ๋ช…์‹œ ์ฃผ์ž…, Python ์ธก `__file__` ์˜์กด์„ฑ ์ œ๊ฑฐ. + - ํšŒ๊ท€ ๊ฐ€๋“œ: `tests/test_b19_headless_reconcile_fixes.py` (6๊ฐœ ๊ธฐ๋Šฅ/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ 100% ํ†ต๊ณผ). ### **B-14 (โœ… ์™„๋ฃŒ โ€” F-1 / P1): `publish_event.py` ๋ธŒ๋กœ์ปค ์žฅ์•  ์‹œ `return 2` ์กฐ๊ธฐ ํƒˆ์ถœ๋กœ ์ธํ•œ 65๋ถ„ ๋ฃจํ”„ ์ •์ง€** - **ํ˜„์ƒ**: `publish_event.py`์—์„œ ๋ธŒ๋กœ์ปค ๋„คํŠธ์›Œํฌ ์žฅ์•  ๋ฐœ์ƒ ์‹œ `return 2`๋กœ ์กฐ๊ธฐ ์ข…๋ฃŒ๋˜์–ด, ๋’ค๋”ฐ๋ฅด๋Š” ๋กœ์ปฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒํƒœ(`update_job_status(status=completed)`) ๋ฐ ๊ฐ์‚ฌ ๋กœ๊ทธ(`append_event`, `registry.append_event`) ๊ฐฑ์‹ ์ด ๋ˆ„๋ฝ๋˜๋˜ ๊ฒฐํ•จ. diff --git a/deploy/INSTALL.md b/deploy/INSTALL.md index 8dbc407..5c6a3d3 100644 --- a/deploy/INSTALL.md +++ b/deploy/INSTALL.md @@ -130,7 +130,7 @@ $ bash .agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh --remov ### 7) ์ „์šฉ NATS ๋ฉ”์‹œ์ง• ๋ธŒ๋กœ์ปค ์„ค์ • (.mam.env) MAM์€ ๋น„๋™๊ธฐ ์ž‘์—… ์œ„์ž„(`multi-agent-mux-delegate-job`) ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ค‘๊ณ„๋ฅผ ์œ„ํ•ด MQTT 3.1.1 ๋ฐ JetStream ๊ธฐ๋ฐ˜์˜ ์‚ฌ์„ค NATS ๋ธŒ๋กœ์ปค(`nats-docker`)๋ฅผ ํ‘œ์ค€์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. * **ํ™˜๊ฒฝ ์„ค์ • ์ƒ์„ฑ**: `bash deploy/generate-env.sh` (๋˜๋Š” `cp .mam.env.example .mam.env`)๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋กœ์ปฌ `.mam.env`๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. -* **์„œ๋ธŒ๋ชจ๋“ˆ ๋™๊ธฐํ™”**: `git submodule update --init --recursive` ๋ช…๋ น์–ด๋กœ `nats-docker/` ๋ฐฐํฌ ์ž์‚ฐ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. +* **์„œ๋ธŒ๋ชจ๋“ˆ ๋™๊ธฐํ™”**: `git submodule update --init --recursive` ๋ช…๋ น์–ด๋กœ `nats-docker/` ๋ฐฐํฌ ์ž์‚ฐ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. (์‚ฌ๋‚ด ๋น„๊ณต๊ฐœ ์ €์žฅ์†Œ `laa/nats-docker` ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†๋Š” ๊ฒฝ์šฐ ์„œ๋ธŒ๋ชจ๋“ˆ ๋™๊ธฐํ™”๋ฅผ ์ƒ๋žตํ•ด๋„ ํ‘œ์ค€ MQTT ๋ธŒ๋กœ์ปค๋ฅผ ํ†ตํ•ด ๊ธฐ๋ณธ ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋Šฅ์ด ์™„๋น„๋ฉ๋‹ˆ๋‹ค.) * **์‚ฌ์„ค ์„œ๋ฒ„ ๋ฐฐํฌ ๊ฐ€์ด๋“œ**: ์ž์„ธํ•œ ๋„์ปค ๋ฐฐํฌ ๋ฐ Tailscale ์—ฐ๋™ ์ ˆ์ฐจ๋Š” [`nats-docker/PRIVATE_SERVER.md`](../nats-docker/PRIVATE_SERVER.md) ๋ฐ [`MESSAGING.md`](../MESSAGING.md)๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค. --- diff --git a/deploy/README.md b/deploy/README.md index 770a82c..08aa663 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -72,13 +72,16 @@ To register these skills globally or for a specific workspace: For production deployments and private networks, MAM utilizes a dedicated NATS broker (`nats:2.12-alpine` with MQTT 3.1.1 and JetStream enabled). The container assets and deployment guides are managed in the `nats-docker` submodule: ```bash -# When cloning the repository, initialize submodules: +# When cloning the repository with internal credentials: git clone --recurse-submodules https://git.godopu.com/tmpl/multi-agent-mux.git # Or initialize submodules in an existing clone: git submodule update --init --recursive ``` +> [!NOTE] +> `nats-docker` is an optional submodule hosted in the private repository `laa/nats-docker`. If cloning without internal credentials, omit `--recurse-submodules`. The MAM framework functions out-of-the-box using standard MQTT brokers configured in `.mam.env`. + Refer to [`nats-docker/PRIVATE_SERVER.md`](../nats-docker/PRIVATE_SERVER.md) and [`MESSAGING.md`](../MESSAGING.md) for detailed configuration, `.mam.env` generation, and security guidelines. --- diff --git a/tests/test_b19_headless_reconcile_fixes.py b/tests/test_b19_headless_reconcile_fixes.py index 3867c81..d1b01fb 100644 --- a/tests/test_b19_headless_reconcile_fixes.py +++ b/tests/test_b19_headless_reconcile_fixes.py @@ -4,60 +4,38 @@ import json import subprocess import pytest +from lib_py.layout import compute_2xk_layout + + def test_bug2_headless_layout_does_not_overflow(): - """Verify Bug 2: w=0, h=0 in headless mode outputs 'right' (not 'overflow').""" - calc_script = """ -import sys, json, os -min_cols = int(os.environ.get('MAM_MIN_COLS', 60)) -min_rows = int(os.environ.get('MAM_MIN_ROWS', 20)) -try: - d = json.loads(sys.stdin.read()).get('result', {}) - focused_id = d.get('focused_pane_id', '') - panes = d.get('panes', []) - anchor = None - for p in panes: - if p.get('pane_id') == focused_id: - anchor = p.get('rect', {}) - break - if not anchor and panes: - anchor = panes[0].get('rect', {}) - if anchor: - w = anchor.get('width', 0) - h = anchor.get('height', 0) - if w <= 0 or h <= 0: - print('right') - elif w // 2 >= min_cols: - print('right') - elif h // 2 >= min_rows: - print('down') - else: - print('overflow') -except Exception: - pass -""" + """Verify Bug 2: w=0, h=0 in headless mode does not trigger overflow.""" # 1. Headless 0x0 - payload_0x0 = json.dumps({"result": {"panes": [{"rect": {"width": 0, "height": 0}}]}}) - res = subprocess.run([sys.executable, "-c", calc_script], input=payload_0x0, capture_output=True, text=True) - assert res.stdout.strip() == "right", f"Headless 0x0 should default to 'right', got {res.stdout.strip()}" + payload_0x0 = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 0, "height": 0}}]}} + d_0x0 = compute_2xk_layout(payload_0x0) + assert not d_0x0.is_overflow, f"Headless 0x0 should not overflow, got {d_0x0}" + assert d_0x0.direction in ("right", "down"), f"Headless 0x0 direction must be right or down, got {d_0x0.direction}" # 2. Genuine small pane (overflow) - payload_small = json.dumps({"result": {"panes": [{"rect": {"width": 50, "height": 30}}]}}) - res = subprocess.run([sys.executable, "-c", calc_script], input=payload_small, capture_output=True, text=True) - assert res.stdout.strip() == "overflow", f"Small pane should be 'overflow', got {res.stdout.strip()}" + payload_small = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 50, "height": 30}}]}} + d_small = compute_2xk_layout(payload_small) + assert d_small.is_overflow, f"Small pane should be overflow, got {d_small}" + assert d_small.direction == "overflow" # 3. Wide pane (split right) - payload_wide = json.dumps({"result": {"panes": [{"rect": {"width": 160, "height": 30}}]}}) - res = subprocess.run([sys.executable, "-c", calc_script], input=payload_wide, capture_output=True, text=True) - assert res.stdout.strip() == "right", f"Wide pane should be 'right', got {res.stdout.strip()}" + payload_wide = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 160, "height": 30}}]}} + d_wide = compute_2xk_layout(payload_wide) + assert not d_wide.is_overflow + assert d_wide.direction == "right" # 4. Tall pane (split down) - payload_tall = json.dumps({"result": {"panes": [{"rect": {"width": 80, "height": 60}}]}}) - res = subprocess.run([sys.executable, "-c", calc_script], input=payload_tall, capture_output=True, text=True) - assert res.stdout.strip() == "down", f"Tall pane should be 'down', got {res.stdout.strip()}" + payload_tall = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 80, "height": 60}}]}} + d_tall = compute_2xk_layout(payload_tall) + assert not d_tall.is_overflow + assert d_tall.direction == "down" def test_bug3_reconcile_skills_dir_passed_and_fallback(): - """Verify Bug 3: reconcile.sh passes SKILLS_DIR to env_python/atomic_dump_yaml and RECON_SRC has fallback.""" + """Verify Bug 3: reconcile.sh evaluates valid SKILLS_DIR and passes it to Python subshells.""" recon_path = os.path.abspath(".agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh") with open(recon_path, "r", encoding="utf-8") as f: content = f.read() @@ -66,9 +44,32 @@ def test_bug3_reconcile_skills_dir_passed_and_fallback(): assert 'SKILLS_DIR="$SKILLS_DIR" LIB_SH="$LIB_SH" env_python' in content assert 'SKILLS_DIR="$SKILLS_DIR" LIB_SH="$LIB_SH" atomic_dump_yaml' in content - # Assert fallback exists inside RECON_SRC - assert "if not skills_dir:" in content - assert "skills_dir = os.path.join(_ws_root, '.agents/skills')" in content + # Read the actual line from reconcile.sh and verify it uses && pwd instead of || pwd + line19 = next(l for l in content.splitlines() if l.startswith("SKILLS_DIR=")) + assert "&& pwd" in line19 and "|| pwd" not in line19, f"Invalid SKILLS_DIR evaluation: {line19}" + + # Functionally evaluate that exact line from reconcile.sh in bash + script = f"""#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(dirname "{recon_path}")" +{line19} +echo "RESOLVED_SKILLS_DIR=$SKILLS_DIR" +if [ -z "$SKILLS_DIR" ]; then + echo "ERROR: SKILLS_DIR is empty" >&2 + exit 1 +fi +if [ ! -d "$SKILLS_DIR" ]; then + echo "ERROR: directory does not exist" >&2 + exit 1 +fi +if [ ! -f "$SKILLS_DIR/lib.sh" ]; then + echo "ERROR: lib.sh missing" >&2 + exit 1 +fi +""" + res = subprocess.run(["bash", "-c", script], capture_output=True, text=True) + assert res.returncode == 0, f"Script failed: {res.stderr}" + assert "RESOLVED_SKILLS_DIR=" in res.stdout def test_bug4_send_keys_safe_gating_order(): @@ -89,7 +90,7 @@ def test_bug4_send_keys_safe_gating_order(): assert dialog_idx != -1, "_pane_dialog_open not found in send_keys_safe" assert prompt_idx != -1, "agent prompt not found in send_keys_safe" - # Crucial ordering check: quiescence and dialog checks MUST precede agent prompt + # Ordering check: quiescence and dialog checks MUST precede agent prompt assert quiescent_idx < prompt_idx, "_pane_quiescent must execute before agent prompt fast-path" assert dialog_idx < prompt_idx, "_pane_dialog_open must execute before agent prompt fast-path" @@ -126,3 +127,108 @@ echo "SUCCESS" assert res.returncode == 0, f"Expected clean exit 0 without duplicate paste-buffer call, got {res.returncode}. Stderr: {res.stderr}" assert "SUCCESS" in res.stdout + +def test_bug4_headless_unobservable_fast_path(tmp_path): + """Verify Bug 4 / R-1: in headless mode where capture-pane is empty, send_keys_safe bypasses dialogs and succeeds immediately via RPC fast-path.""" + test_script = f"""#!/usr/bin/env bash +set -euo pipefail +SKILL_DIR="{os.path.abspath('.agents/skills')}" +source "$SKILL_DIR/lib.sh" + +PROMPT_CALLED=0 +PASTE_CALLED=0 +_sks_herdr() {{ + if [ "${{1:-}}" = "capture-pane" ]; then + # Headless / unobservable pane returns empty output + echo "" + return 0 + fi + if [ "${{1:-}}" = "agent" ] && [ "${{2:-}}" = "prompt" ]; then + PROMPT_CALLED=1 + return 0 + fi + if [ "${{1:-}}" = "paste-buffer" ]; then + PASTE_CALLED=1 + return 0 + fi + return 0 +}} + +# Run send_keys_safe without stubbing _pane_quiescent +send_keys_safe "headless-sess" "my prompt" "job-headless" +if [ "$PROMPT_CALLED" != "1" ]; then + echo "ERROR: agent prompt was not called in headless mode" >&2 + exit 1 +fi +if [ "$PASTE_CALLED" = "1" ]; then + echo "ERROR: paste-buffer was called unexpectedly" >&2 + exit 1 +fi +echo "HEADLESS_OK" +""" + res = subprocess.run(["bash", "-c", test_script], capture_output=True, text=True) + assert res.returncode == 0, f"Headless send_keys_safe failed: {res.stderr}" + assert "HEADLESS_OK" in res.stdout + + +def test_bug4_slow_settling_pane_success(tmp_path): + """Verify N-1 / G-2: a pane that takes 3 seconds of changing output to settle stabilizes cleanly and executes RPC prompt.""" + count_file = str(tmp_path / "capture_count.txt") + with open(count_file, "w") as f: + f.write("0") + + prompt_flag = str(tmp_path / "prompt_called.txt") + paste_flag = str(tmp_path / "paste_called.txt") + + test_script = f"""#!/usr/bin/env bash +set -euo pipefail +SKILL_DIR="{os.path.abspath('.agents/skills')}" +source "$SKILL_DIR/lib.sh" + +COUNT_FILE="{count_file}" +PROMPT_FLAG="{prompt_flag}" +PASTE_FLAG="{paste_flag}" + +_sks_herdr() {{ + if [ "${{1:-}}" = "capture-pane" ]; then + local c + c=$(cat "$COUNT_FILE" 2>/dev/null || echo "0") + c=$((c + 1)) + echo "$c" > "$COUNT_FILE" + # Change for first 5 captures (2.5s), then stabilize + if [ "$c" -le 5 ]; then + echo "Rendering frame $c..." + else + echo "Stable Idle Screen" + fi + return 0 + fi + if [ "${{1:-}}" = "agent" ] && [ "${{2:-}}" = "prompt" ]; then + touch "$PROMPT_FLAG" + return 0 + fi + if [ "${{1:-}}" = "paste-buffer" ]; then + touch "$PASTE_FLAG" + return 0 + fi + return 0 +}} + +# Run send_keys_safe on slow-settling pane with default 20x0.5 window +send_keys_safe "slow-sess" "my prompt" "job-slow" +if [ ! -f "$PROMPT_FLAG" ]; then + echo "ERROR: agent prompt was not called on slow-settling pane" >&2 + exit 1 +fi +if [ -f "$PASTE_FLAG" ]; then + echo "ERROR: paste-buffer was called unexpectedly" >&2 + exit 1 +fi +echo "SLOW_SETTLE_OK" +""" + res = subprocess.run(["bash", "-c", test_script], capture_output=True, text=True) + assert res.returncode == 0, f"Slow settling pane failed: {res.stderr}" + assert "SLOW_SETTLE_OK" in res.stdout + + +