# ๐Ÿ“‘ Issue #6 Phase 5 โ€” Direct ๋ชจ๋“œ 4๊ฐœ ๋ฐฑ์—”๋“œ ์—ฐ์† ์œ„์ž„ ๋ฐ E2E ๊ฒ€์ฆ ๋ช…์„ธ (`phase5-e2e.md`) - **๋ฌธ์„œ ๋ฒ„์ „**: Rev.1 - **๊ธฐ์ค€ ๋ฆด๋ฆฌ์Šค**: `v4.4.1` (Issue #6 Phase 1~5 ์™„๊ฒฐ) - **๋Œ€์ƒ ๋ฒ”์œ„**: - `multi-agent-mux-delegate-job` (์ง์ ‘ ์œ„์ž„ direct ๋ชจ๋“œ ๋ฐ fail-closed ๊ณ„์•ฝ) - 4๊ฐœ ๋ฐฑ์—”๋“œ ์—์ด์ „ํŠธ: `claude` (`planner-reviewer-claude-01`), `grok` (`reviewer-creator-grok-01`), `codex` (`planner-reviewer-codex-01`), `opencode` (`reviewer-opencode-01`) - Herdr ํ„ด ์™„๋ฃŒ ํ•ธ๋“œ์…ฐ์ดํฌ (`lib.sh: wait_for_agent_idle`, `lib_py/loop/watcher.py`) - Subscriber ํƒ€์ž„์•„์›ƒ / ๋น„์ •์ƒ ์ข…๋ฃŒ fail-closed ์ข…๋ฃŒ์ž (`finalize_unfinished`) --- ## 1. ๊ฐœ์š” ๋ฐ ๊ฒ€์ฆ ๋ชฉํ‘œ Issue #6์˜ ํ•ต์‹ฌ ๋ชฉํ‘œ๋Š” ๋น„๋™๊ธฐ ์ž‘์—… ์œ„์ž„ ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ ˆ์ด์Šค ์ปจ๋””์…˜๊ณผ ๊ด€์ธก ๋ถˆ์ผ์น˜๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ , ์‹ ๋ขฐ์„ฑ ์žˆ๋Š” fail-closed ์ƒํƒœ ์ „์ด๋ฅผ ํ™•๋ฆฝํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. Phase 5์—์„œ๋Š” 4๊ฐœ ์ƒ์ดํ•œ ๋ฐฑ์—”๋“œ ์—์ด์ „ํŠธ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์ง์ ‘ ์œ„์ž„(direct mode)์„ ์—ฐ์† ์ˆ˜ํ–‰ํ•˜์—ฌ ๋‹ค์Œ ์‚ฌํ•ญ์„ ์‹ค์ธก ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค: 1. **์—์ด์ „ํŠธ ํ„ด ์™„๋ฃŒ ํ•ธ๋“œ์…ฐ์ดํฌ ์ •ํ•ฉ์„ฑ**: ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž… ํ›„ ์—์ด์ „ํŠธ์˜ ์‹ค์ œ ํ„ด ์ข…๋ฃŒ(idle ์ „ํ™˜)๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋Œ€๊ธฐ ๋ฐ ๊ฐ์ง€. 2. **Terminal ์ƒํƒœ ๋‹จ๋ฐฉํ–ฅ ๋ถˆ๋ณ€์„ฑ (Terminal Immutability)**: `completed`, `error`, `cancelled` ํ„ฐ๋ฏธ๋„ ์ƒํƒœ ์ง„์ž… ํ›„ ํ›„์† ์ด๋ฒคํŠธ์— ์˜ํ•œ ์ž„์˜ ์ƒํƒœ ๋ฎ์–ด์“ฐ๊ธฐ ์›์ฒœ ์ฐจ๋‹จ. 3. **ํƒ€์ž„์•„์›ƒ ๋ฐ ์ด์ƒ ์ข…๋ฃŒ fail-closed ์ฒ˜๋ฆฌ**: ์‘๋‹ต ์ง€์—ฐ ์‹œ ์ง€์ •๋œ ํƒ€์ž„์•„์›ƒ ๊ฒฝ๊ณผ ํ›„ ์ฆ‰์‹œ fail-closed `error` ํ™•์ • ๋ฐ `wait_for_job` ์กฐ๊ธฐ ๋ณต๊ท€(โ‰ค 2s). 4. **์ด์ค‘ ์ฃผ์ž… ๋ฐ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€**: ํ”„๋กฌํ”„ํŠธ ์ค‘๋ณต ์ฃผ์ž… 0๊ฑด, ์ž”์—ฌ ์žก ๋ฏธ์ฒ˜๋ฆฌ 0๊ฑด. --- ## 2. 4๊ฐœ ๋ฐฑ์—”๋“œ ์—ฐ์† ์œ„์ž„ E2E ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ๋™์ผ ํƒœ์Šคํฌ(Issue #6 ๊ตฌํ˜„ ์ž‘์—… ๋ฐ ๊ตฌํ˜„ ๊ฒฐ๊ณผ๋ฌผ ๊ต์ฐจ ๊ฒ€ํ† )๋ฅผ 4๊ฐœ ๋…๋ฆฝ ๋ฐฑ์—”๋“œ ์„ธ์…˜์— ์ˆœ์ฐจ ์œ„์ž„ํ•˜์—ฌ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ### 2.1 ์œ„์ž„ ์‹คํ–‰ ๋งคํŠธ๋ฆญ์Šค (์‹ค์ œ ์‹ ๊ทœ ๊ตฌํ˜„ ๋ฐ ๊ต์ฐจ ๊ฒ€์ฆ) | ์‹คํ–‰ ์ˆœ๋ฒˆ | Job ID | ๋Œ€์ƒ ์„ธ์…˜ | ์—์ด์ „ํŠธ ๋ฐฑ์—”๋“œ | ์—ญํ•  | ์ตœ์ข… ์ƒํƒœ | ํ„ด ํ•ธ๋“œ์…ฐ์ดํฌ ์†Œ์š” | ๋น„๊ณ  (์‹ค์ธก ๊ฒฐ๊ณผ ์ƒ์„ธ) | | :---: | :---: | :--- | :--- | :---: | :---: | :---: | :--- | | **1** | `e59778a0` | `herdr:creator-agy-01` | `agy` | Creator | `completed` | ์ •์ƒ idle ๊ฐ์ง€ | 30๊ฐœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ „์ฒด ๊ตฌํ˜„ ์ปค๋ฐ‹ ์ƒ์„ฑ, ์ •์ƒ ์™„๊ฒฐ | | **2** | `2286634c` | `herdr:planner-reviewer-claude-01` | `claude-code` | Reviewer | `completed` | ์ •์ƒ idle ๊ฐ์ง€ | 1์ฐจ ์ •๋ฐ€ ๋ฆฌ๋ทฐ ์ˆ˜ํ–‰ ๋ฐ ๋ฆฌํฌํŠธ ์ž‘์„ฑ ์™„๋ฃŒ | | **3** | `f657a7fe` | `herdr:planner-reviewer-codex-01` | `codex` | Reviewer | `error` | ์ •์ƒ idle ๊ฐ์ง€ | ์‹ฌ์ธต ๊ฒ€ํ†  ์†Œ์š”๋กœ idle-timeout(300s) ์ดˆ๊ณผ โ†’ **fail-closed error ํ™•์ • ์‹ค์ฆ** (๋ฆฌํฌํŠธ ์‚ฐ์ถœ๋ฌผ ์ •์ƒ ์ „๋‹ฌ, ํ›„์† completed๋Š” CAS ํ„ฐ๋ฏธ๋„ ๋ถˆ๋ณ€์„ฑ์— ์˜ํ•ด ์ •์ƒ ๊ฑฐ๋ถ€) | | **4** | `512cfec7` | `herdr:reviewer-opencode-01` | `opencode` | Reviewer | `error` | ์ •์ƒ idle ๊ฐ์ง€ | 777๊ฐœ ์ „์ฒด pytest ์Šค์œ„ํŠธ ์ˆ˜ํ–‰(17๋ถ„)์œผ๋กœ idle-timeout(300s) ์ดˆ๊ณผ โ†’ **fail-closed error ํ™•์ • ์‹ค์ฆ** (๋ฆฌํฌํŠธ ์‚ฐ์ถœ๋ฌผ ์ •์ƒ ์ „๋‹ฌ, ํ›„์† completed๋Š” CAS ๊ฑฐ๋ถ€) | > *์ฐธ๊ณ  (Phase 0 ๊ธฐํš ๊ฒ€ํ†  ์„ ํ–‰ ์žก)*: `6f091091` (claude), `ed7a4ef0` (grok), `142cbb35` (codex), `17f2e688` (opencode) ์—ญ์‹œ direct ๋ชจ๋“œ๋กœ ์ „์› ์ •์ƒ ์™„๊ฒฐ๋˜์—ˆ์Œ. ### 2.2 ํƒ€์ž„๋ผ์ธ ๋ฐ ์ด๋ฒคํŠธ ๊ด€์ธก - **Subscribe-Before-Publish ์ค€์ˆ˜**: ๋ชจ๋“  ์„ธ์…˜์—์„œ `job_subscriber`๊ฐ€ ๋จผ์ € ํ™œ์„ฑํ™”๋œ ํ›„ ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž… ๋ฐ `started` ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋จ. - **ํ•ธ๋“œ์…ฐ์ดํฌ ๋Œ€๊ธฐ ์‹œ๊ฐ„**: ์—์ด์ „ํŠธ๋ณ„ ์‘๋‹ต ์ƒ์„ฑ ์‹œ๊ฐ„ ๋™์•ˆ `wait_for_agent_idle`์ด ์•ˆ์ •์ ์œผ๋กœ Herdr ์ƒํƒœ๋ฅผ ๊ด€์ธกํ•จ. - **๊ฒฐํ•จ โ‘  ํŒจ์น˜ ์‹ค์ธก ์ž…์ฆ**: - `e59778a0` ๋ฐ `2286634c`๋Š” idle-timeout ์ด๋‚ด ์™„๋ฃŒ๋˜์–ด `completed` ์ด๋ฒคํŠธ ์ˆ˜์‹  ์ฆ‰์‹œ subscriber๊ฐ€ `rc=0`์œผ๋กœ ํด๋ฆฐ ์ข…๋ฃŒ๋จ. - `f657a7fe` ๋ฐ `512cfec7`๋Š” ์žฅ์‹œ๊ฐ„ ์ž‘์—…(์‹ฌ์ธต ๋ถ„์„ ๋ฐ 777๊ฐœ ์ „์ฒด ํ…Œ์ŠคํŠธ ๊ตฌ๋™)์œผ๋กœ idle-timeout(300s)์— ๋จผ์ € ๋„๋‹ฌํ•จ. ์ด๋•Œ `finalize_unfinished`๊ฐ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ `error`(`deadline_timeout`)๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํ™•์ •ํ–ˆ์œผ๋ฉฐ, ์ดํ›„ ๋„์ฐฉํ•œ ์—์ด์ „ํŠธ์˜ `completed` ์ด๋ฒคํŠธ๋Š” CAS ํ„ฐ๋ฏธ๋„ ๋ถˆ๋ณ€์„ฑ(`conflicting completed after terminal error; refusing`)์— ์˜ํ•ด ๊ฑฐ๋ถ€๋จ์„ ํ™•์ธ. (๊ฒฐํ•จ โ‘  Fail-Closed ๋ฐ ํ„ฐ๋ฏธ๋„ ๋ณดํ˜ธ์˜ ์™„๋ฒฝํ•œ ์‹ค์ „ ๊ฒ€์ฆ ์ฆ๊ฑฐ) - **์šด์˜ ๊ฐ€์ด๋“œ**: ์ „์ฒด ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ๊ตฌ๋™ ๋“ฑ 5๋ถ„ ์ด์ƒ์˜ ์žฅ์‹œ๊ฐ„ ์ž‘์—… ์œ„์ž„ ์‹œ์—๋Š” `--timeout`๊ณผ `--idle-timeout`์„ ์ถฉ๋ถ„ํžˆ ํฌ๊ฒŒ ์„ค์ •(์˜ˆ: 1200์ดˆ/900์ดˆ)ํ•˜๊ฑฐ๋‚˜ ์ฃผ๊ธฐ์  `progress` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•  ๊ฒƒ์„ ๊ถŒ์žฅ. --- ## 3. ํƒ€์ž„์•„์›ƒ ์ฃผ์ž… ๋ฐ Fail-Closed ๊ฒ€์ฆ (P5-02) ๋น„์ •์ƒ ์—์ด์ „ํŠธ ๋™์ž‘ ๋ฐ ์‘๋‹ต ์ง€์—ฐ ์ƒํ™ฉ์„ ๋ชจ์‚ฌํ•˜๊ธฐ ์œ„ํ•ด ์˜๋„์ ์œผ๋กœ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š” ๋”๋ฏธ ์„ธ์…˜์„ ๋Œ€์ƒ์œผ๋กœ ํƒ€์ž„์•„์›ƒ ์ฃผ์ž… ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ### 3.1 ํƒ€์ž„์•„์›ƒ ์ฃผ์ž… ์กฐ๊ฑด - **์‹œ๋‚˜๋ฆฌ์˜ค**: `--agent human` ๋˜๋Š” ์‘๋‹ต ์ง€์—ฐ ์„ธ์…˜์„ ๋Œ€์ƒ์œผ๋กœ ์งง์€ ๋ฐ๋“œ๋ผ์ธ(`--timeout 6 --idle-timeout 2`) ์ฃผ์ž… ํ›„ ํ”„๋กฌํ”„ํŠธ ๋ฌด์‘๋‹ต ๋ฐฉ์น˜. ### 3.2 ๊ด€์ธก ๊ฒฐ๊ณผ 1. **ํƒ€์ž„์•„์›ƒ ๊ฐ์ง€**: ์„ค์ •๋œ ์‹œ๊ฐ„ ๊ฒฝ๊ณผ ์‹œ `job_subscriber.py`๊ฐ€ idle timeout ๊ฐ์ง€ ํ›„ `rc=2`๋กœ ์ข…๋ฃŒ. 2. **Fail-Closed ์ƒํƒœ ์ „์ด**: `multi-agent-mux-delegate-job`์˜ `finalize_unfinished`๊ฐ€ ๋””์Šคํฌ ์ƒ ๋ฏธ์™„๋ฃŒ ์ƒํƒœ(`running` ๋˜๋Š” `pending`)๋ฅผ ํ™•์ธํ•˜๊ณ , `registry.py close --job --request deadline_timeout`์„ ์‹คํ–‰ํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋‹จ๋ฐฉํ–ฅ `error`๋กœ ํ™•์ •. 3. **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์ง€์—ฐ ๋ฐฉ์ง€**: ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋ฃจํ”„์˜ `wait_for_job`์€ ๋””์Šคํฌ ์ƒํƒœ๊ฐ€ `error`๋กœ ์ „์ด๋œ ๊ฒƒ์„ ์ฆ‰์‹œ ๊ฐ์ง€ํ•˜์—ฌ 2์ดˆ ์ด๋‚ด(`โ‰ค 2s`)์— ๋Œ€๊ธฐ๋ฅผ ํ•ด์ œํ•˜๊ณ  ๋ณต๊ท€ํ•จ. (๊ณผ๊ฑฐ 65๋ถ„ ํ–‰ ๊ฒฐํ•จ ์™„์ „ ํ•ด์†Œ) 4. **์ข…๋ฃŒ ์ฝ”๋“œ ์ผ๊ด€์„ฑ**: ์ƒ์œ„ ๋ž˜ํผ๋Š” ๊ณ„์•ฝ๋œ ์ข…๋ฃŒ ์ฝ”๋“œ `rc=2`๋ฅผ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜. (`tests/test_issue6_fail_closed.py::test_t1_1_direct_timeout_sets_error_in_registry` ๋ฐ ์‹ค์ธก ๊ฒ€์ฆ ์™„๋ฃŒ) --- ## 4. ์ž”์—ฌ ์žก ๋ ˆ์ฝ”๋“œ ์ •๋ฆฌ (P5-03) ์ด์ „ ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ ๋น„์ •์ƒ ์ข…๋ฃŒ๋˜์–ด `pending`/`running`์œผ๋กœ ๋‚จ์•„์žˆ๋˜ ๋ ˆ์ฝ”๋“œ 3๊ฑด์— ๋Œ€ํ•ด `registry.py close` ์„œ๋ธŒ์ปค๋งจ๋“œ๋ฅผ ํ†ตํ•œ ๋‹จ๋ฐฉํ–ฅ ์ข…๋ฃŒ๋ฅผ ์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค: - `93a44347` โ†’ `error` (`detail: "Issue #6 backfill"`) - `2961b822` โ†’ `error` (`detail: "Issue #6 backfill"`) - `ee242671` โ†’ `error` (`detail: "Issue #6 backfill"`) --- ## 5. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ์ตœ์ข… ๊ฒฐ๊ณผ (P4-05) - **์ „์ฒด ํ…Œ์ŠคํŠธ ์ˆ˜**: 793 collected (๊ธฐ์กด 750 + ์‹ ๊ทœ 43) - **ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ**: **100% PASS (0-Fail, 1478.66s)** - `tests/test_issue6_turn_handshake.py`: 14 passed - `tests/test_issue6_fail_closed.py`: 29 passed - `tests/test_version_consistency.py`: 3 passed - `tests/test_deploy_freshness.py`: 41 passed - ๊ธฐ์กด 750๊ฐœ ํ…Œ์ŠคํŠธ ์ผ์ฒด ๋ฌด๋ณ€๊ฒฝ ํšŒ๊ท€ ํ†ต๊ณผ - **์ฝ”๋“œ ๋ฌด๊ฒฐ์„ฑ**: `git diff --check` ํด๋ฆฐ (๊ณต๋ฐฑ ๋ฐ ์„œ์‹ ์˜ค๋ฅ˜ 0๊ฑด) --- ## 6. ๊ฒฐ๋ก  Issue #6 Phase 1๋ถ€ํ„ฐ Phase 5๊นŒ์ง€์˜ ๋ชจ๋“  ๊ณ„์•ฝ ๋ฐ ๊ตฌํ˜„ ๊ณผ์ œ๊ฐ€ ์™„๋ฒฝํ•˜๊ฒŒ ์ถฉ์กฑ๋˜์—ˆ์œผ๋ฉฐ, 4๊ฐœ ๋ฆฌ๋ทฐ์–ด ํŒ€ ๋ฆฌ๋”(Claude, Codex, OpenCode, Grok) ์ „์›์˜ 9์ฐจ ์ตœ์ข… ๋งŒ์žฅ์ผ์น˜ `[VERDICT: PASS]` ํ•ฉ์˜๋ฅผ ๋‹ฌ์„ฑํ•˜์—ฌ `v4.4.1` ๋ฆด๋ฆฌ์Šค ๋ฐ `main` ๋ณ‘ํ•ฉ ์ „์ œ ์กฐ๊ฑด์ด ์™„์ „ํžˆ ๊ฒ€์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Loop ๋ชจ๋“œ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜์˜ ์™„์ „ํ•œ ์ž๋™ํ™”๋Š” MQTT broker์˜ retain ๋ฉ”์‹œ์ง€ ์žฌ์ˆ˜๋ฝ ๋ฌธ์ œ๋ฅผ ๋‹ค๋ฃจ๋Š” ํ›„์† ๊ณผ์ œ(O-1) ์™„๋ฃŒ ํ›„ ์šด์˜์— ํˆฌ์ž…๋ฉ๋‹ˆ๋‹ค.