# ๐Ÿ“‘ Issue #6 Implementation Plan (Phase 1โ€“5 Execution & Gate Specification) - **Document Version**: Rev.2 (Planner `planner-reviewer-claude-01`, job `f4873d82` โ€” Creator `creator-agy-01` ์ด์˜์ œ๊ธฐ 3๊ฑด ๋ฐ˜์˜; Rev.1์€ job `d2d70f57`) - **Issue**: [Gitea Issue #6 โ€” `[Bug] delegate-job ์›Œ์ปค ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฏธ๊ฐฑ์‹ (Fail-Silent) ๋ฐ TUI ํ„ด ๋งˆ๊ฐ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ํ•ด๊ฒฐ`](https://git.godopu.com/tmpl/multi-agent-mux/issues/6) - **Source of Truth**: `.agents/reports/issue_6_consolidated_review_report.md` (Rev. 2) โ€” 4์ธ ๋ฆฌ๋ทฐ์–ด ํ•ฉ์˜ ์‚ฌ์–‘ - **Baseline Commit**: `f98b958` (`main`, v4.4.0), Herdr `0.8.2`, Python 3.9.6, ์ „์ฒด ์Šค์œ„ํŠธ 750 passed - **Status**: ์ฐฉ์ˆ˜ ์ „ (Phase 1 ๋Œ€๊ธฐ) --- ## 1. ๊ฐœ์š” ๋ฐ ์„ค๊ณ„ ์›์น™ ๋ณธ ๋ฌธ์„œ๋Š” Rev.2 ํ•ฉ์˜ ์‚ฌ์–‘(ยง4)์„ **ํŒŒ์ผยทํ•จ์ˆ˜ยท๋ผ์ธ ๋‹จ์œ„์˜ ์‹คํ–‰ ๊ณ„ํš**์œผ๋กœ ์ „๊ฐœํ•˜๊ณ , ๋‹จ๊ณ„๋ณ„ ์™„๋ฃŒ ๊ฒŒ์ดํŠธยท๋กค๋ฐฑยท๋น„๋ฒ”์œ„๋ฅผ ๊ทœ์ •ํ•œ๋‹ค. ๋ชจ๋“  ๋ผ์ธ ๋ฒˆํ˜ธ๋Š” ๊ธฐ์ค€ ์ปค๋ฐ‹ `f98b958` ๊ธฐ์ค€์ด๋ฉฐ, ์ฐฉ์ˆ˜ ์‹œ `grep -n`์œผ๋กœ ์žฌํ™•์ธํ•œ๋‹ค. ### ๐Ÿ“Œ ํ•ต์‹ฌ ์›์น™ (Rev.2 ยง4 ํ•ฉ์˜) 1. **๊ธฐ์กด CAS ์—”์ง„ ์žฌ์‚ฌ์šฉ**: ํƒ€์ž„์•„์›ƒ fail-closed๋Š” `engine.py:50`์˜ `deadline_timeout` ์š”์ฒญ(๋น„-operator, terminal ์ดํ›„ ๊ฑฐ๋ถ€)์œผ๋กœ๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ ๊ทœ `--expect-status` ํ”Œ๋ž˜๊ทธ์™€ operator `error` ์˜ค๋ฒ„๋ผ์ด๋“œ๋Š” ๋„์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค. 2. **์‚ฌ์ „ ํ•ธ๋“œ์…ฐ์ดํฌ ์ˆœ์„œ**: TUI ์œ ํœด ๋Œ€๊ธฐ(`wait_for_agent_idle`)๋Š” **๊ตฌ๋…์ž(`job_subscriber.py`) ๊ธฐ๋™ ์ด์ „**์— ์™„๋ฃŒํ•œ๋‹ค. ๊ตฌ๋…์ž idle ์‹œ๊ณ„(`job_subscriber.py:265-267`, subscribe ์‹œ์  ๊ธฐ์‚ฐ)๋ฅผ ์ž ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. 3. **์œ ํœด ์˜ˆ์‚ฐ 120์ดˆ**: `SKS_IDLE_TIMEOUT_MS` ๊ธฐ๋ณธ `120000`(์‹ค์ธก: codex `completed`โ†’`done` 31s). 4. **๋ธŒ๋กœ์ปค ์žฅ์•  ๊ฒฉ๋ฆฌ (B-15 ๊ณ„์•ฝ)**: ๊ตฌ๋…์ž `rc=3`์€ ์›Œ์ปค ์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์žก ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ๋””์Šคํฌ๋ฅผ ์žฌ๊ด€์ธกํ•œ ๋’ค์—๋งŒ `deadline_timeout`์„ ์ ์šฉํ•œ๋‹ค. 5. **๋ฆฌํ…Œ์ธ ์˜ค์—ผ ๋ฐฉ์ง€ (O-1)**: loop/discuss ๊ฒฝ๋กœ์—์„œ๋Š” ๋ž˜ํผ๊ฐ€ MQTT `error` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค(๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ „์ด๋งŒ). direct ๊ฒฝ๋กœ๋Š” ๊ฐ์‚ฌยท๋ชจ๋‹ˆํ„ฐ์šฉ์œผ๋กœ ๋ฐœํ–‰ํ•œ๋‹ค. 6. **๋กค๋ฐฑ ์Šค์œ„์น˜**: ๊ฐ ํ–‰์œ„ ๋ณ€๊ฒฝ์€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฆ‰์‹œ ์ด์ „ ๋™์ž‘์œผ๋กœ ๋ณต๊ท€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค (Issue #5 ๊ด€๋ก€). 7. **๋ฏธ์ฃผ์ž… ์‹คํŒจ๋Š” `error`๊ฐ€ ์•„๋‹ˆ๋‹ค (Rev.2 Secondary)**: ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํ•œ ๊ธ€์ž๋„ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ)๋Š” `deadline_timeout`์ด ์•„๋‹ˆ๋ผ **`cancelled`**(detail `session_busy`)๋กœ ๋‹ซ๊ณ  ๋ž˜ํผ rc=5๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด์กดํ•˜๋˜ ๋ ˆ์ฝ”๋“œ๋Š” ๋น„์ข…๋ฃŒ ์ƒํƒœ๋กœ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. 8. **์…ธโ†’Python ๊ฒฝ๊ณ„์—์„œ herdr ๊ฒฉ๋ฆฌ ๋ช…์‹œ ์ „๋‹ฌ (Rev.2 Primary)**: Python ์ชฝ `herdr` ํ˜ธ์ถœ์€ ๋ฐ˜๋“œ์‹œ `--session "$HERDR_SESSION_NAME"`์„ ๋ช…์‹œํ•˜๊ณ , ์…ธ ์ชฝ ํ˜ธ์ถœ์€ `mam_herdr`/`_sks_herdr`๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋Š” ํ•ญ์ƒ `"$(_delegate_py_bin)"`์ด๋‹ค. ### ๐Ÿ“Œ ์‹ค์ธก์œผ๋กœ ํ™•์ •๋œ ์‚ฌ์‹ค (๊ณ„ํš์˜ ์ „์ œ) | ์‚ฌ์‹ค | ๊ทผ๊ฑฐ | |---|---| | direct ๊ฒฝ๋กœ `wait "$sub_pid" \|\| true`๋Š” `:227`, loop ์›Œ์ปค/๋ฆฌ๋ทฐ์–ด ์กฐ๊ธฐ ํƒˆ์ถœ์€ `:359`/`:375` | `grep -n` (1์ฐจ ๋ณด๊ณ ์„œ์˜ `:213`์€ ์˜ค๊ธฐ) | | ๋ช…์‹œ์  ์ฃผ์ž… ์‹คํŒจ(`send_keys_safe` rcโ‰ 0)๋Š” `set -e`๋กœ exit 1 + EXIT trap(`:523`)์ด `error` ๋ฐœํ–‰ โ†’ **๊ธฐ๋ณธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” ์ด๋ฏธ fail-closed** | ์‹ค์ œ ๋ž˜ํผ์— rc=4 ์ฃผ์ž… ์‹ค์ธก | | ๋‹จ, trap ๋ช…๋ น์— `--registry-dir`๊ฐ€ ์—†์–ด ์ปค์Šคํ…€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” `job record not found`๋กœ `pending` ์ž”์กด | ๋™์ผ ์‹ค์ธก | | ๋ž˜ํผ exit ํ›„ ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ณ ์•„๋กœ ์ž”์กด | ๋™์ผ ์‹ค์ธก (`pgrep job_subscriber.py`) | | `deadline_timeout`: pendingโ†’error ์ปค๋ฐ‹, completedโ†’`conflicting error after terminal completed; refusing` | `engine.transition` ์ง์ ‘ ํ˜ธ์ถœ ์‹ค์ธก | | ์žก ์™„๋ฃŒ ํ›„ ์—์ด์ „ํŠธ ์ƒํƒœ๋Š” `done`(idle ์•„๋‹˜); working ์ค‘์—๋„ `interactive_ready: true`, `โฏ` ๋ Œ๋”๋ง | `herdr agent list`/ํŽ˜์ธ ์บก์ฒ˜ ์‹ค์ธก | | `herdr agent wait`๋Š” ์ด๋ฏธ ๋ชฉํ‘œ ์ƒํƒœ๋ฉด ์ฆ‰์‹œ rc=0, ์•„๋‹ˆ๋ฉด timeout ์‹œ rc=1 + stderr JSON | ์‹ค์ธก, `lib_py/loop/watcher.py` ๋ชจ๋“ˆ docstring๊ณผ ์ผ์น˜ | | `dispatch_token`์€ `claim`/`redispatch`์—์„œ๋งŒ ๋ฐœ๊ธ‰(`engine.py:87,297-300`) โ†’ direct ์žก์€ ํ† ํฐ ์—†์Œ, loop 2ํšŒ์ฐจ๋ถ€ํ„ฐ ์กด์žฌ | ์ฝ”๋“œ | | **์‹ค์ œ `herdr` ๋ฐ”์ด๋„ˆ๋ฆฌ๋Š” `HERDR_SESSION`ยท`HERDR_SOCKET_PATH` ํ™˜๊ฒฝ๋ณ€์ˆ˜์™€ `--session ` ํ”Œ๋ž˜๊ทธ๋งŒ ์ธ์‹ํ•˜๋ฉฐ, MAM์˜ `HERDR_SESSION_NAME`์€ ์ธ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค.** `HERDR_SESSION_NAME`์€ `lib.sh`์˜ `herdr()`/`mam_herdr`/`_real_herdr`(`lib.sh:234-243`)๊ฐ€ `--session`์œผ๋กœ ๋ฒˆ์—ญํ•˜๋Š” ์…ธ ํ•จ์ˆ˜ ์ „์šฉ ๋ณ€์ˆ˜๋ผ **Python `subprocess`์—๋Š” ์ƒ์†๋˜์ง€ ์•Š๋Š”๋‹ค** (Rev.2 Primary-A) | `env -u โ€ฆ herdr agent get` 5์กฐํ•ฉ ์‹ค์ธก: `HERDR_SESSION_NAME`๋งŒ โ†’ `server_not_running`(๊ธฐ๋ณธ ์†Œ์ผ“), `HERDR_SESSION`/`HERDR_SOCKET_PATH`/`--session` โ†’ ์ •์ƒ | | `lib.sh`๋ฅผ sourceํ•œ ์…ธ์—์„œ๋„ Python `subprocess.run(["herdr", โ€ฆ])`๋Š” `/opt/homebrew/bin/herdr`๋กœ ํ•ด์„๋˜์–ด ๊ธฐ๋ณธ ์†Œ์ผ“์„ ์กฐํšŒํ•œ๋‹ค | ๋™์ผ ์‹ค์ธก (f) | | **loop ๋ชจ๋“œ ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์€ v4.4.0์—์„œ ์ด๋ฏธ ๊นจ์ ธ ์žˆ๋‹ค**: `:276-283`์˜ `update --status pending --iteration $iteration`์ด ์›Œ์ปค `completed` ํ›„ ๊ฐ™์€ iteration์œผ๋กœ `redispatch`๋ฅผ ์š”์ฒญ โ†’ `redispatch iteration 1 does not advance past 1` rc=4 โ†’ `set -e`๋กœ ๋ž˜ํผ ์ข…๋ฃŒ (Rev.2 Tertiary) | `registry.py` CLI ์‹ค์ธก (iteration 2๋ฉด rc=0) | | `cancelled` ์š”์ฒญ์€ ๋น„-operator ์†Œ์Šค๋กœ pendingโ†’cancelled ์ปค๋ฐ‹, completed ์ดํ›„ ๊ฑฐ๋ถ€, ์ดํ›„ operator `redispatch`(iteration ์ „์ง„)๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ | `engine.transition` ์‹ค์ธก | --- ## 2. ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ ๋‹จ์œ„ ๋ฐ ํ•„์ˆ˜ ์™„๋ฃŒ ๊ธฐ์ค€ (Roadmap & Gates) | ๋‹จ๊ณ„ | ์ž‘์—… ๋ชฉํ‘œ | ์„ธ๋ถ€ ๊ตฌํ˜„ (ยง3 ์ฐธ์กฐ) | ํ•„์ˆ˜ ์™„๋ฃŒ ๊ฒŒ์ดํŠธ | ๋กค๋ฐฑ | |---|---|---|---|---| | **Phase 1** | **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CAS ํ‘œ์ค€ ์—ฐ๋™ ๋ฐ delegate-job Fail-Closed** | โ€ข `registry.py close --request deadline_timeout\|cancelled` ์„œ๋ธŒ์ปค๋งจ๋“œ
โ€ข ๋ž˜ํผ `finalize_unfinished`/`reap_subscriber` ํ—ฌํผ, direct(`:227`)ยทloop(`:359`,`:375`)ยท`run_agent` ์‹คํŒจยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ 5๊ฐœ ์ข…๋ฃŒ ๊ฒฝ๋กœ ์ผ์›ํ™”
โ€ข rc=3 ๋””์Šคํฌ ์žฌ๊ด€์ธก, trap `--registry-dir`, dry-run ์ˆœ์„œ, ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ(rc 5 `session_busy` ํฌํ•จ)
โ€ข **loop ์—ญํ•  ์ „ํ™˜ `attempt_id` ๊ทœ์•ฝ** (ยง3.1.6) | โ€ข `--agent human` ๊ฒฐ์ •์  ์žฌํ˜„ 4์ข…(ยง5 T1-1~T1-4)์ด ์ˆ˜์ • ์ „ ์‹คํŒจยท์ˆ˜์ • ํ›„ ํ†ต๊ณผ
โ€ข T1-10(์›Œ์ปค ์™„๋ฃŒโ†’๋ฆฌ๋ทฐ์–ด ์žฌ๋””์ŠคํŒจ์น˜ ์„ฑ๊ณต) ์ˆ˜์ • ์ „ rc=4ยท์ˆ˜์ • ํ›„ rc=0
โ€ข `test_wrapper_exit_code_mapping_literals_are_frozen`ยทF-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข `MAM_DELEGATE_FAIL_CLOSED=0`์—์„œ ๊ธฐ์กด exit 0 ๋™์ž‘ ์žฌํ˜„ | `MAM_DELEGATE_FAIL_CLOSED=0` (ํ—ฌํผ no-op, `\|\| true` ๋ณต์›) | | **Phase 2** | **์‚ฌ์ „ TUI ์œ ํœด ํ•ธ๋“œ์…ฐ์ดํฌ ๋ฐ ์ œ์ถœ ๊ฒ€์ฆ ๋ณด๊ฐ•** | โ€ข `lib_py/loop/watcher.py` CLI ์ง„์ž…์ (`"$(_delegate_py_bin)" -m lib_py.loop.watcher wait โ€ฆ --session "$HERDR_SESSION_NAME"`)
โ€ข `lib.sh: wait_for_agent_idle` (์‚ฌ์ „ `mam_herdr agent get` โ†’ `unknown`/`blocked` ๋ถ„๊ธฐ โ†’ watcher CLI, `SKS_IDLE_RETRIES` ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„, ์ตœ์ข… ์‹คํŒจ rc 1 โ†’ ๋ž˜ํผ rc 5 `cancelled`)
โ€ข ๋ž˜ํผ `run_agent`๋ฅผ `preflight_agent`+`inject_agent`๋กœ ๋ถ„๋ฆฌ, ์ˆœ์„œ: preflight โ†’ idle wait โ†’ subscriber โ†’ inject
โ€ข `send_keys_safe` ํด๋ฐฑ ์ œ์ถœ ๊ฒ€์ฆ์—์„œ pre-submit busy ํ† ํฐ ๋ฐฐ์ œ, `agent prompt --wait --until working` ์ˆ˜๋ฝ ํ™•์ธ(์žฌ์ œ์ถœ ๊ธˆ์ง€) | โ€ข mock_herdr ์ƒํƒœ ์‹œํ€€์Šค ํ…Œ์ŠคํŠธ ยง5 T2-1~T2-11 ํ†ต๊ณผ
โ€ข `test_h16`(Enter ์ •ํ™•ํžˆ 1ํšŒ)ยท`test_b8`ยท`test_c1_*` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข ๋ž˜ํผ ๋กœ๊ทธ์—์„œ `SUBSCRIBED` ์ด์ „์— idle ๋Œ€๊ธฐ ์™„๋ฃŒ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋จ
โ€ข T2-10: watcher CLI argv์— `--session` ํฌํ•จ, T2-11: `lib.sh`์— bare `python`/`python3 -m lib_py.loop` ํ˜ธ์ถœ 0๊ฑด(์ •์  ๋ฆฐํŠธ) | `SKS_IDLE_HANDSHAKE=0` (๊ฒŒ์ดํŠธ ์Šคํ‚ต, ๊ธฐ์กด `_pane_quiescent`๋งŒ ์‚ฌ์šฉ), `SKS_PROMPT_WAIT=0` | | **Phase 3** | **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ์—ฐ๊ฒฐ** | โ€ข `runner.delegate_job`: rcโ‰ 0์—์„œ๋„ `registered job:` ํŒŒ์‹ฑ, `created_jobs`ยทcheckpoint ๊ธฐ๋ก, id ๋ฐ˜ํ™˜
โ€ข **rc=5(`session_busy`)๋Š” `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30) ๋Œ€๊ธฐ ํ›„ 1ํšŒ ์žฌ์œ„์ž„(์ƒˆ job id)**, ์žฌ์‹คํŒจ ์‹œ None
โ€ข ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด ๋””์Šคํฌ `error`/`cancelled`๋กœ 1s ๋‚ด False
โ€ข `run_loop.sh` ๋ ˆ๊ฑฐ์‹œ `delegate_job_safe` ๋™์ผ ํŒŒ์‹ฑ(`MAM_LOOP_PY=0` ๊ฒฝ๋กœ ๋ณด์กด) | โ€ข `LoopRunner.wait_for_job`์ด fail-closed ์žก์—์„œ โ‰ค 2s ๋ฐ˜ํ™˜(T3-1)
โ€ข ์‹คํŒจ job id๊ฐ€ `.mam/loop-checkpoint.json`์— ๊ธฐ๋ก(T3-2)
โ€ข rc=5 1ํšŒ ์žฌ์œ„์ž„ ํ›„ ์„ฑ๊ณต/์‹คํŒจ ๋ถ„๊ธฐ(T3-3)
โ€ข `test_issue5_phase3_loop.py` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | ํ•จ์ˆ˜ ๋‹จ์œ„ revert (ํ”Œ๋ž˜๊ทธ ๋ถˆํ•„์š”: rcโ‰ 0 ๊ฒฝ๋กœ๋งŒ ๋ณ€๊ฒฝ) | | **Phase 4** | **๋‹จ์œ„/๊ณ„์•ฝ ํšŒ๊ท€ ์Šค์œ„ํŠธ** | โ€ข `tests/test_issue6_fail_closed.py`, `tests/test_issue6_turn_handshake.py`
โ€ข `tests/conftest.py` mock_herdr์— `agent_status`ยท`agent wait`ยท`agent prompt --wait` ๋ชจํ‚น
โ€ข ๋ฌธ์„œ: SKILL.md ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ, MESSAGING.md ยง5.1 ๋ณด๊ฐ• | โ€ข ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ์ „๋ถ€ **์ˆ˜์ • ์ „ ์ฝ”๋“œ์—์„œ ์‹คํŒจํ•จ์„ ์—ญ๊ฒ€์ฆ**(Issue #5 ๊ด€๋ก€)
โ€ข ์ „์ฒด ์Šค์œ„ํŠธ 750 + ์‹ ๊ทœ โ‰ฅ 20, 0-Fail
โ€ข `git diff --check` ํด๋ฆฐ | ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์‚ญ์ œ๋งŒ์œผ๋กœ ์›๋ณต | | **Phase 5** | **์‹ค๊ธฐ๋™ 4๊ฐœ ์—์ด์ „ํŠธ E2E ๋ฐ ๋ฆด๋ฆฌ์Šค** | โ€ข Claude/Grok/Codex/OpenCode ๊ฐ 1ํšŒ: `completed` ์งํ›„ ๋™์ผ ์„ธ์…˜ ์—ฐ์† ์œ„์ž„ ํƒ€์ž„๋ผ์ธ ๊ธฐ๋ก
โ€ข ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…(์—์ด์ „ํŠธ ๋ฌด์‘๋‹ต)์œผ๋กœ 65๋ถ„ ๋Œ€์‹  โ‰ค `IDLE_TIMEOUT`+5s ๋‚ด `error` ํ™•์ธ
โ€ข ์ž”์กด ๋ ˆ์ฝ”๋“œ 3๊ฑด ํ์‡„, IMPROVEMENTS.md ์›์žฅ, VERSIONS.md `v4.4.1` 4-way ๋ฝ์Šคํ… | โ€ข 4๊ฐœ ๋ฐฑ์—”๋“œ ์—ฐ์† ์œ„์ž„ ์ฃผ์ž… ์‹คํŒจ 0๊ฑด, ์ค‘๋ณต ์ œ์ถœ 0๊ฑด
โ€ข `test_version_consistency.py` ํ†ต๊ณผ
โ€ข ๋ฆฌ๋ทฐ์–ด ๊ต์ฐจ ๊ฒ€ํ†  `[VERDICT: PASS]` | ๋ฆด๋ฆฌ์Šค ํƒœ๊ทธ ๋ฏธ๋ถ€์—ฌ ์ƒํƒœ๋กœ `main` revert | --- ## 3. ์ƒ์„ธ ๊ตฌํ˜„ ์‚ฌ์–‘ ### 3.1 Phase 1 โ€” Fail-Closed **3.1.1 `lib_py/delegation/registry.py` โ€” `close` ์„œ๋ธŒ์ปค๋งจ๋“œ (์‹ ๊ทœ, Rev.2: `timeout`์„ ์ผ๋ฐ˜ํ™”)** ```text registry.py [--registry-dir D] close --job --request {deadline_timeout,cancelled} [--detail ] [--expect-dispatch-token ] ``` - ๊ตฌํ˜„: `engine.transition(job_id, request, registry_dir=rd, source="delegate_wrapper", fields={"detail": detail}, expected_dispatch_token=tok_or_None)`. ๋‘ ์š”์ฒญ ๋ชจ๋‘ ๋น„-operator์ด๋ฉฐ terminal ์ดํ›„ ๊ฑฐ๋ถ€๋œ๋‹ค(์‹ค์ธก). - ์šฉ๋„ ๊ตฌ๋ถ„: `deadline_timeout` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋œ ๋’ค** ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์—†์ด ๋ฐ๋“œ๋ผ์ธ ์†Œ์ง„(์‹คํ–‰ ์‹คํŒจ). `cancelled` + detail `session_busy` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋˜๊ธฐ ์ „** ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ; Rev.2 Secondary). ํ›„์ž๋Š” `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅํ•˜๋‹ค. - ์ข…๋ฃŒ ์ฝ”๋“œ: ์ปค๋ฐ‹ โ†’ 0; ๊ฑฐ๋ถ€ ์‚ฌ์œ ๊ฐ€ `conflicting โ€ฆ after terminal` ๋˜๋Š” ๋ฉฑ๋“ฑ โ†’ **0**(stderr์— ์‚ฌ์œ  1์ค„; ์ด๋ฏธ ๋‹ซํžŒ ์žก์€ ์ •์ƒ); ๋ ˆ์ฝ”๋“œ ์—†์Œ โ†’ 1; ํ† ํฐ ๋ถˆ์ผ์น˜ โ†’ 4. - `status --set`๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค(`_operator_write` ๋ฐ F-C ๋ฉฑ๋“ฑ ๊ณ„์•ฝ ๋ณด์กด). **3.1.2 ๋ž˜ํผ `multi-agent-mux-delegate-job` โ€” ๊ณตํ†ต ํ—ฌํผ (์‹ ๊ทœ, `run_agent` ์œ„์— ์ •์˜)** ```bash # reap_subscriber โ€” ์ž๊ธฐ ๊ตฌ๋…์ž๋งŒ ํšŒ์ˆ˜. ์›๋ž˜ rc๋Š” ํ˜ธ์ถœ์ž๊ฐ€ ๋ณด์กด. reap_subscriber() { kill "$1" 2>/dev/null || true; wait "$1" 2>/dev/null || true; } # finalize_unfinished [] # completed -> no-op (rc 0) # broker_unavailable -> ๋‚จ์€ ๋ฐ๋“œ๋ผ์ธ(TIMEOUT - ๊ฒฝ๊ณผ) ๋™์•ˆ 3s ๊ฐ„๊ฒฉ ๋””์Šคํฌ ์žฌ๊ด€์ธก; # completed/error ๋ฐœ๊ฒฌ ์‹œ ๊ทธ ์ƒํƒœ ์ฑ„ํƒ, ์•„๋‹ˆ๋ฉด deadline_timeout # ๊ทธ ์™ธ(timeout/error/running) -> ์ฆ‰์‹œ registry.py close --request deadline_timeout # phase == preflight|idle_wait -> registry.py close --request cancelled --detail session_busy (rc 5) # (ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ: deadline_timeout ๊ธˆ์ง€ โ€” Rev.2 Secondary) # direct ๊ฒฝ๋กœ์—์„œ๋งŒ publish_event --event error|cancelled ์ถ”๊ฐ€ ๋ฐœํ–‰ (loop/discuss ๊ธˆ์ง€: O-1) # MAM_DELEGATE_FAIL_CLOSED=0 ์ด๋ฉด ์ „๋ถ€ no-op (๋กค๋ฐฑ) ``` - `disk_st` ์žฌ๊ด€์ธก์€ ๊ธฐ์กด `:342-347`์˜ ์ธ๋ผ์ธ ํŒŒ์ด์ฌ์„ ํ•จ์ˆ˜ `_disk_status`๋กœ ์ถ”์ถœํ•ด ์žฌ์‚ฌ์šฉ. - `expected_dispatch_token`์€ **๊ตฌ๋…์ž ๊ธฐ๋™ ์ง์ „** ๋ ˆ์ฝ”๋“œ์—์„œ ์ฝ์–ด ์ง€์—ญ๋ณ€์ˆ˜์— ๋ณด๊ด€(`_disp_tok`), ์กด์žฌํ•  ๋•Œ๋งŒ ์ „๋‹ฌ(direct ์žก์€ ์—†์Œ). **3.1.3 ์ข…๋ฃŒ ๊ฒฝ๋กœ 5๊ฐœ ์ผ์›ํ™”** | # | ์œ„์น˜ (f98b958) | ๋ณ€๊ฒฝ | |---|---|---| | E1 | direct `:227` `wait "$sub_pid" \|\| true` | `wait "$sub_pid" \|\| sub_rc=$?` โ†’ ๊ธฐ์กด `:340-354` ๋งคํ•‘ ๋ธ”๋ก์„ ํ•จ์ˆ˜ `_map_sub_rc`๋กœ ์ถ”์ถœ(๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด ๋ณด์กด) โ†’ `finalize_unfinished` โ†’ `exit_code=$(_rc_for "$job_status")` | | E2 | loop ์›Œ์ปค `:359-362` | `break` ์ง์ „ `finalize_unfinished "$JOB_ID" "$job_status" "$sub_rc" worker` | | E3 | loop ๋ฆฌ๋ทฐ์–ด `:375-378` | ๋™์ผ (`reviewer`) | | E4 | `run_agent` ์‹คํŒจ (`:327` ๋ฐ direct `:204`) | `if ! run_agent โ€ฆ; then reap_subscriber "$sub_pid"; finalize_unfinished "$JOB_ID" error 1 inject; exit 1; fi`. trap(`:523`)์€ `--registry-dir "$REGISTRY_DIR"` ์ถ”๊ฐ€ ํ›„ ์ตœํ›„ ๋ณด๋ฃจ๋กœ๋งŒ ์œ ์ง€ | | E5 | ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ(`:456-473` herdr/์„ธ์…˜ ๋ถ€์žฌ, `:511-513` strict-role) ๋ฐ ์œ ํœด ๋Œ€๊ธฐ ์ตœ์ข… ์‹คํŒจ | Phase 2์—์„œ `preflight_agent`๋กœ ์ด๋™. ์‹คํŒจ ์‹œ ์žก ๋“ฑ๋ก ํ›„๋ผ๋ฉด `finalize_unfinished โ€ฆ preflight\|idle_wait` โ†’ `cancelled`(session_busy) ํ›„ **`exit 5`** | **3.1.4 ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ (SKILL.md์— ๋ฌธ์„œํ™”)** | rc | ์˜๋ฏธ | |---|---| | 0 | `completed` | | 1 | `error` (์—์ด์ „ํŠธ ๋ฐœํ–‰ / ์ฃผ์ž…ยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ) | | 2 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ โ†’ `deadline_timeout` ์ ์šฉ๋จ | | 3 | ๋ธŒ๋กœ์ปค ์žฅ์•  + ๋ฐ๋“œ๋ผ์ธ ๋‚ด ๋””์Šคํฌ ๋ฏธํ™•์ธ โ†’ `deadline_timeout` ์ ์šฉ๋จ | | 5 | `session_busy`: ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ/์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋กœ ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ โ†’ `cancelled` ์ ์šฉ๋จ (์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ) | ๋งˆ์ง€๋ง‰ stdout ์ค„(๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฒฝ๋กœ)์€ ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์œ ์ง€ํ•œ๋‹ค(`runner.delegate_job`/`run_loop.sh:373` ํŒŒ์‹ฑ ๊ณ„์•ฝ). **3.1.5 dry-run ์ˆœ์„œ (๊ตฌ๋…์ž ๊ธฐ๋™ `:166` โ†’ dry-run `return 0` `:224`)**: `DRY_RUN=1`์ด๋ฉด ๊ตฌ๋…์ž๋ฅผ ๊ธฐ๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค(ํ˜„์žฌ๋Š” ๊ธฐ๋™ ํ›„ `return 0`์œผ๋กœ ๊ณ ์•„ ๋ฐœ์ƒ). **3.1.6 loop/discuss ์—ญํ•  ์ „ํ™˜์˜ `attempt_id` ๊ทœ์•ฝ (Rev.2 Tertiary โ€” ๊ธฐ์กด ๊ฒฐํ•จ ์ˆ˜์ •)** - ๋ฌธ์ œ: `:276-283`์€ ์›Œ์ปคยท๋ฆฌ๋ทฐ์–ด phase ๋ชจ๋‘ ๊ฐ™์€ `$iteration`์œผ๋กœ `update --status pending`(=`redispatch`)์„ ์š”์ฒญํ•œ๋‹ค. `engine.py:227-240`์€ terminal์—์„œ์˜ `redispatch`์— `iteration` ์ „์ง„์„ ์š”๊ตฌํ•˜๋ฏ€๋กœ ์›Œ์ปค `completed` ์งํ›„ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์ด rc=4๋กœ ๊ฑฐ๋ถ€๋˜๊ณ  `set -e`๋กœ ๋ž˜ํผ๊ฐ€ ์ฃฝ๋Š”๋‹ค(์‹ค์ธก). F-A ํ…Œ์ŠคํŠธ(`test_issue5_phase2a_loop.py:727`)๋Š” iteration 2๋ฅผ ๋„˜๊ฒจ ์ด ๊ฒฝ๋กœ๋ฅผ ๋น„์ผœ๊ฐ„๋‹ค. - ์„ค๊ณ„: `iteration`์€ ๋ฃจํ”„ ํšŒ์ฐจ ๋ผ๋ฒจ๋กœ ์œ ์ง€ํ•˜๊ณ , **๋””์ŠคํŒจ์น˜ ๋‹จ์กฐ ์นด์šดํ„ฐ `attempt_id`**๋ฅผ ๋„์ž…ํ•œ๋‹ค(์—”์ง„ `:171`์ด ์ด๋ฏธ `record.get("attempt_id", record.get("iteration"))`์œผ๋กœ ์ฝ๋Š” ํ•„๋“œ). 1. ๋ž˜ํผ: ์žก๋ณ„ `attempt=$((attempt+1))`์„ ๊ฐ phase ๋””์ŠคํŒจ์น˜ ์ง์ „์— ์ฆ๊ฐ€์‹œํ‚ค๊ณ  `update โ€ฆ --attempt-id "$attempt"`๋ฅผ ํ•จ๊ป˜ ๋„˜๊ธด๋‹ค(์›Œ์ปค r1=1, ๋ฆฌ๋ทฐ์–ด r1=2, ์›Œ์ปค r2=3 โ€ฆ). 2. `registry.py update`์— `--attempt-id` ์ถ”๊ฐ€(`:296-302`). 3. `engine._decide` redispatch ๊ทœ์น™: `fields`์— `attempt_id`๊ฐ€ ์žˆ์œผ๋ฉด **`attempt_id`๊ฐ€ `record.attempt_id`(์—†์œผ๋ฉด `iteration`)๋ณด๋‹ค ์ปค์•ผ** ์ปค๋ฐ‹; ์—†์œผ๋ฉด ๊ธฐ์กด `iteration` ๊ทœ์น™ ์œ ์ง€(F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ). `attempt_id`๋Š” `redispatch`/`claim` ์ปค๋ฐ‹ ์‹œ ๋ ˆ์ฝ”๋“œ์— ๊ธฐ๋ก. 4. `publish_event.py:250-255`์˜ `--finalize-report` ๊ธฐ๋ณธ `attempt_id`๋Š” `record.attempt_id`๋ฅผ ์šฐ์„ ํ•˜๊ณ  ์—†์œผ๋ฉด `iteration`. - ๋ฒ”์œ„ ์ฃผ์˜: ์ด ์ˆ˜์ •์œผ๋กœ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์˜ CAS ๊ฑฐ๋ถ€๋Š” ์‚ฌ๋ผ์ง€์ง€๋งŒ, **loop ๋ชจ๋“œ์˜ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1(๋ฆฌํ…Œ์ธ ์žฌ์ˆ˜๋ฝ, ยง4)์— ์˜ํ•ด ์—ฌ์ „ํžˆ ์ฆ‰์‹œ ์ข…๋ฃŒ๋œ๋‹ค.** ๋”ฐ๋ผ์„œ Phase 5 E2E๋Š” direct ๋ชจ๋“œ๋งŒ ์ˆ˜ํ–‰ํ•˜๊ณ , loop ๋ชจ๋“œ ์ข…๋‹จ ๊ฒ€์ฆ์€ O-1 ์ด์Šˆ ์™„๋ฃŒ ํ›„๋กœ ๋ช…์‹œํ•œ๋‹ค. ### 3.2 Phase 2 โ€” ์‚ฌ์ „ TUI ํ•ธ๋“œ์…ฐ์ดํฌ **3.2.1 `lib_py/loop/watcher.py` CLI ์ง„์ž…์  (์‹ ๊ทœ `main`)** ```text "$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session [--until S]... [--timeout-ms N] stdout: {"kind": "matched|timeout|agent_not_found|transport_error", "agent_status": "...", "error_code": "..."} rc: matched 0 / timeout 2 / agent_not_found 3 / transport_error 4 ``` - `wait_once`๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ(์…ธ ์ค‘๋ณต ๊ตฌํ˜„ ๊ธˆ์ง€). `lib_py.delegation`์€ `lib_py.loop`๋ฅผ ์ž„ํฌํŠธํ•˜์ง€ ์•Š๋Š”๋‹ค(ํŒจํ‚ค์ง€ ๊ทœ์น™ ์œ ์ง€) โ€” ํ˜ธ์ถœ์ž๋Š” ์…ธ(`lib.sh`)์ด๋‹ค. - **๊ฒฉ๋ฆฌ ์ „๋‹ฌ (Rev.2 Primary-A)**: `build_argv(target, until, timeout_ms, session=None)`์— `session` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๊ฐ’์ด ์žˆ๊ณ  `default`๊ฐ€ ์•„๋‹ˆ๋ฉด argv๋ฅผ `["herdr", "--session", session, "agent", "wait", โ€ฆ]`๋กœ ๊ตฌ์„ฑํ•œ๋‹ค. CLI `--session`์€ ๋ฏธ์ง€์ • ์‹œ `HERDR_SESSION_NAME` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ๋Š”๋‹ค. ์‹ค์ œ `herdr`๋Š” `HERDR_SESSION_NAME`์„ ์ธ์‹ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ(์‹ค์ธก) ์ด ๋ช…์‹œ ์ „๋‹ฌ์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์†Œ์ผ“(`~/.config/herdr/herdr.sock`)์„ ์กฐํšŒํ•ด `server_not_running`/`agent_not_found`๊ฐ€ ๋œ๋‹ค. ๊ธฐ์กด `wait_once` ํ˜ธ์ถœ์ž(`shadow.py` ๋“ฑ)๋Š” `session=None`์œผ๋กœ ๋™์ž‘ ๋ถˆ๋ณ€. - **์ธํ„ฐํ”„๋ฆฌํ„ฐ (Rev.2 Primary-B)**: bare `python`/`python3` ๊ธˆ์ง€. `lib.sh`๋Š” `"$(_delegate_py_bin)"`(`lib.sh:1841`)๋งŒ ์‚ฌ์šฉํ•˜๋ฉฐ `PYTHONPATH`๋Š” `lib.sh:25,164`๊ฐ€ ์ด๋ฏธ ๋ณด์žฅํ•œ๋‹ค. T2-11 ์ •์  ๋ฆฐํŠธ๋กœ ๊ณ ์ •. **3.2.2 `lib.sh: wait_for_agent_idle [timeout_ms]` (์‹ ๊ทœ, `send_keys_safe` ์œ„)** 1. `SKS_IDLE_HANDSHAKE=0` โ†’ return 0 (๋กค๋ฐฑ). 2. `_sks_herdr agent get `(=`mam_herdr`, ๊ฒฉ๋ฆฌ ์ธ์ง€ ์…ธ ํ•จ์ˆ˜)๋กœ `agent_status` ์กฐํšŒ: `unknown`/์กฐํšŒ ์‹คํŒจ โ†’ return 3(ํœด๋ฆฌ์Šคํ‹ฑ ํด๋ฐฑ ์‹ ํ˜ธ, ํ˜ธ์ถœ์ž๋Š” ๊ธฐ์กด `_pane_quiescent` ๊ฒฝ๋กœ ์ง„ํ–‰); `blocked` โ†’ return 2(์ฃผ์ž… ๊ธˆ์ง€; `_pane_dialog_open` ์ฒ˜๋ฆฌ๋กœ ์œ„์ž„). 3. `idle`/`done` โ†’ return 0. `working` โ†’ `"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session "$HERDR_SESSION_NAME" --until idle --until done --until blocked --timeout-ms ${SKS_IDLE_TIMEOUT_MS:-120000}`: matched(idle/done) โ†’ 0, matched(blocked) โ†’ 2, timeout โ†’ ์žฌ์‹œ๋„, ๊ทธ ์™ธ โ†’ 3. 4. **์žฌ์‹œ๋„ (Rev.2 Secondary)**: timeout์€ `SKS_IDLE_RETRIES`(๊ธฐ๋ณธ 2)ํšŒ๊นŒ์ง€ `SKS_IDLE_RETRY_BACKOFF_S`(๊ธฐ๋ณธ 10)์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ˜๋ณตํ•œ๋‹ค(๊ธฐ๋ณธ ์ด ์˜ˆ์‚ฐ โ‰ˆ 3ร—120s + 20s). ์ตœ์ข… ์‹คํŒจ โ†’ return 1("agent still working"). ํ˜ธ์ถœ์ž(๋ž˜ํผ)๋Š” rc 1์„ `finalize_unfinished โ€ฆ idle_wait`(โ†’ `cancelled`/rc 5)๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉฐ **`deadline_timeout`์„ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค**. - `interactive_ready`๋Š” ์–ด๋–ค ํŒ์ •์—๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. **3.2.3 ๋ž˜ํผ ์‹คํ–‰ ์ˆœ์„œ ์žฌ๋ฐฐ์น˜ (`cmd_submit`)** ```text register โ†’ brief ์ž‘์„ฑ โ†’ preflight_agent(herdr ์กด์žฌยท์„ธ์…˜ ์กด์žฌยท์—ญํ•  ๊ฒ€์‚ฌ) โ†’ wait_for_agent_idle "$sess" (rc 1/2 โ†’ finalize_unfinished โ€ฆ idle_wait โ†’ cancelled/session_busy; exit 5) โ†’ _disp_tok ์ฝ๊ธฐ โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ + SUBSCRIBED ํ•ธ๋“œ์…ฐ์ดํฌ โ†’ inject_agent(send_keys_safe) โ†’ wait sub โ†’ finalize ``` - loop/discuss ๋ฐ˜๋ณต๋งˆ๋‹ค ๋™์ผ ์ˆœ์„œ(`:286` ๊ตฌ๋…์ž ๊ธฐ๋™ ์•ž์— idle wait). **3.2.4 `send_keys_safe` ๋ณด๊ฐ• (`lib.sh:2231-2345`)** - ์ œ์ถœ ๊ฒ€์ฆ(`:2330-2332`): `pre_submit`์— `esc to interrupt`/`โ— |โœฝ |[A-Za-z]+ing`๊ฐ€ **์ด๋ฏธ ์žˆ์—ˆ์œผ๋ฉด** ํ•ด๋‹น ํ† ํฐ์„ ์„ฑ๊ณต ๊ทผ๊ฑฐ์—์„œ ์ œ์™ธํ•˜๊ณ  marker ์†Œ๋ฉธ + ํ™”๋ฉด ๋ณ€ํ™”๋งŒ ์ธ์ •. - ๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๋กœ(`:2279`): `SKS_PROMPT_WAIT=1`(๊ธฐ๋ณธ)์ด๋ฉด `agent prompt --wait --until working --timeout 5000`. ์„ฑ๊ณต โ†’ 0. `agent_prompt_stalled`/timeout โ†’ **์žฌ์ œ์ถœํ•˜์ง€ ์•Š๊ณ ** 2s ํ›„ `agent get` 1ํšŒ: `working` โ†’ 0, ๊ทธ ์™ธ โ†’ rc 4("delivery unknown"). `agent_blocked` โ†’ rc 2. - ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… ์ „ busy ํ† ํฐ ๊ฒŒ์ดํŠธ: `wait_for_agent_idle`์ด 3(unknown)์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ `_pane_tail 8`์˜ busy ํ† ํฐ์„ `SKS_BUSY_TIMEOUT`(๊ธฐ๋ณธ 120s)๊นŒ์ง€ ๋Œ€๊ธฐ. ### 3.3 Phase 3 โ€” ๋Ÿฌ๋„ˆ Fail-Fast - `runner.py:279-289`: `res.returncode != 0`์ด์–ด๋„ stdout์—์„œ `registered job:` ๋งค์น˜ ์‹œ `created_jobs.append` + `_record_checkpoint()` ํ›„ **id ๋ฐ˜ํ™˜**(๋กœ๊ทธ๋Š” error ๋ ˆ๋ฒจ ์œ ์ง€). ๋งค์น˜ ์‹คํŒจ ์‹œ์—๋งŒ `None`. - **rc=5 (`session_busy`, Rev.2 Secondary)**: `delegate_job`์ด `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30)์ดˆ ๋Œ€๊ธฐ ํ›„ **1ํšŒ** ์žฌ์œ„์ž„(์ƒˆ job id ๋“ฑ๋ก)ํ•œ๋‹ค. ์žฌ์‹œ๋„๋„ rc=5๋ฉด ๋งˆ์ง€๋ง‰ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด `cancelled`๋กœ ์ฆ‰์‹œ False โ†’ ๊ธฐ์กด abort ๊ฒฝ๋กœ. ๋ฌดํ•œ ์žฌ์‹œ๋„๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค(๊ต์ฐฉ์˜ ์ƒˆ ํ˜•ํƒœ). - `wait_for_job`์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค(3900s๋Š” ์ตœํ›„ ์•ˆ์ „๋ง; `cancelled`๋Š” ์ด๋ฏธ False ์กฐ๊ฑด). - `run_loop.sh:159-168, 373`: `delegate_job_safe` rcโ‰ 0์—์„œ๋„ ์ถœ๋ ฅ์—์„œ job id ์ถ”์ถœ(๋ ˆ๊ฑฐ์‹œ ๊ฒฝ๋กœ ๋™๋“ฑ์„ฑ). ### 3.4 Phase 4 โ€” ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œ (ยง5 ์ฐธ์กฐ) - `tests/conftest.py` mock_herdr: `agent get`/`list`์— `agent_status`(๊ธฐ๋ณธ `idle`) ์ถ”๊ฐ€; `agent wait`๋Š” ์ƒํƒœ ํŒŒ์ผ์˜ `status_sequence`(์˜ˆ: `["working","working","done"]`)๋ฅผ ํ˜ธ์ถœ๋งˆ๋‹ค ์†Œ๋น„, ์†Œ์ง„ ์‹œ ๋งˆ์ง€๋ง‰ ๊ฐ’ ์œ ์ง€, `--timeout`์€ ์‹œํ€€์Šค ์†Œ์ง„ ์ „ ์ข…๋ฃŒ ์‹œ `{"error":{"code":"timeout"}}` rc 1; `agent prompt --wait`๋Š” `prompt_wait_result` ํ•„๋“œ(`ok|agent_prompt_stalled|agent_blocked`)๋กœ ์ œ์–ด. - ๋ฌธ์„œ: `SKILL.md` "Core Commands"์— ์ข…๋ฃŒ ์ฝ”๋“œ ํ‘œ(ยง3.1.4), `MESSAGING.md` ยง5.1 ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ์˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฐ˜์˜ ์„œ์ˆ , `BOOTSTRAP*.md` ์—์ด์ „ํŠธ ํ”„๋กœํ† ์ฝœ์— "completed ๋ฐœํ–‰ ํ›„ ๋„๊ตฌ ํ˜ธ์ถœ ์—†์ด ํ„ด ์ข…๋ฃŒ" 1๋ฌธ์žฅ. ### 3.5 Phase 5 โ€” E2Eยท์ •๋ฆฌยท๋ฆด๋ฆฌ์Šค - E2E ์ ˆ์ฐจ(**direct ๋ชจ๋“œ๋งŒ**; loop ๋ชจ๋“œ ์ข…๋‹จ์€ O-1 ์™„๋ฃŒ ํ›„): ๊ฐ ๋ฐฑ์—”๋“œ์— `--agent human`์ด ์•„๋‹Œ ์‹ค์„ธ์…˜์œผ๋กœ ์žก A ์œ„์ž„ โ†’ `completed` ์ˆ˜์‹  ์ฆ‰์‹œ ์žก B ์œ„์ž„ โ†’ ๋ž˜ํผ ๋กœ๊ทธ์˜ idle ๋Œ€๊ธฐ ์‹œ๊ฐ„, `herdr agent get` ํƒ€์ž„๋ผ์ธ, B์˜ `started` ์ง€์—ฐ์„ ๊ธฐ๋ก(`docs/contracts/issue-6/phase5-e2e.md`). - ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…: ์„ธ์…˜์— `/exit` ์—†์ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฌด์‹œํ•˜๋„๋ก `--idle-timeout 30`์œผ๋กœ ์œ„์ž„ โ†’ ๋ž˜ํผ rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, `wait_for_job` โ‰ค 2s. - ์ž”์กด ๋ ˆ์ฝ”๋“œ `93a44347`, `2961b822`, `ee242671`: `registry.py close --job --request deadline_timeout --detail "Issue #6 backfill"`. - ๋ฆด๋ฆฌ์Šค: `v4.4.1` (PATCH โ€” ๊ณต๊ฐœ ๊ณ„์•ฝ ์‚ญ์ œ ์—†์Œ, ์ข…๋ฃŒ ์ฝ”๋“œ๋Š” ์˜ค๋™์ž‘ ์ •์ •, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ๋Š” ๋ž˜ํผ ๋‚ด๋ถ€ ๋ฐฐ๊ด€). 4-way ๋ฝ์Šคํ…: `lib.sh:32`, `eager.py:16`, `VERSIONS.md`(๊ฐœ์š”+๋งคํŠธ๋ฆญ์Šค+Changelog), 8๊ฐœ `SKILL.md` frontmatter. `IMPROVEMENTS.md` ยง5์— `Issue-6-FailClosed-TurnHandshake` ๋ธ”๋ก ์ถ”๊ฐ€(ยง6 ํ‘œยทํ—ค๋” ํ‚ค ๋ชฉ๋ก ๋™๊ธฐํ™”). --- ## 4. ๋ช…์‹œ์  ๋น„๋ฒ”์œ„ (Out of Scope) โ€” ๋ณ„๋„ ์ด์Šˆ 1. **O-1 ๋ฆฌํ…Œ์ธ ํ„ฐ๋ฏธ๋„ ์žฌ์ˆ˜๋ฝ**: ์žฌ๋””์ŠคํŒจ์น˜ ํ›„ 2ํšŒ์ฐจ ๊ตฌ๋…์ž๊ฐ€ 1ํšŒ์ฐจ retained `completed`๋ฅผ ์ฆ‰์‹œ ์ˆ˜๋ฝ(์‹ค์ธก). loop ๋ชจ๋“œ ๊ธฐ์กด ๊ฒฐํ•จ. ๋ณธ ์ด์Šˆ์—์„œ๋Š” "loop ๊ฒฝ๋กœ retained ๋ฐœํ–‰ ๊ธˆ์ง€"๋กœ ํšŒํ”ผ๋งŒ ํ•œ๋‹ค. **Rev.2 ์ฃผ์˜: ยง3.1.6์˜ attempt_id ์ˆ˜์ • ํ›„์—๋„ loop ๋ชจ๋“œ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1 ๋•Œ๋ฌธ์— ์ข…๋‹จ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ณ„๋„ ์ด์Šˆ๋ฅผ Issue #6 ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ์— "loop/discuss ๋ชจ๋“œ ์‚ฌ์šฉ ์ „ ํ•„์ˆ˜"๋กœ ๋ช…์‹œํ•œ๋‹ค.** 2. **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์˜ค์—ผ**: `.mam/jobs`์— pytest ์„ธ์…˜๋ช… ๋ ˆ์ฝ”๋“œ 467๊ฑด ๋ˆ„์ถœ ์›์ธ. 3. **์„ธ์…˜๋ณ„ ์ „๋‹ฌ ์ง๋ ฌํ™” ๋ฝ**, **direct ์žก `dispatch_token` ๋ฐœ๊ธ‰**: ํ˜„ ๊ตฌ์กฐ(๋™๊ธฐ ์œ„์ž„ + loop lock)์—์„œ ๋ถˆํ•„์š”. 4. **`wait_for_job` 3900s ๋‹จ์ถ•**: ์ฆ์ƒ ์™„ํ™”์ด๋ฏ€๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค. --- ## 5. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ–‰๋ ฌ (Phase 4 ์‚ฐ์ถœ๋ฌผ) ### 5.1 `tests/test_issue6_fail_closed.py` (๋ธŒ๋กœ์ปค ๋ฌด๊ด€, `--agent human` + ์งง์€ ํƒ€์ž„์•„์›ƒ) | ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | |---|---|---| | T1-1 | direct, ์ด๋ฒคํŠธ ์—†์Œ (`--timeout 6 --idle-timeout 2`) | rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, events.log์— wrapper `error` 1๊ฑด, ๊ตฌ๋…์ž pid ์†Œ๋ฉธ | | T1-2 | loop, ์›Œ์ปค phase ์ด๋ฒคํŠธ ์—†์Œ | rc=2, `error`, **retained ๋ฐœํ–‰ ์—†์Œ** | | T1-3 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ ์งํ›„ `publish_event completed` ๊ฒฝ์Ÿ | ์ตœ์ข… `completed` ์œ ์ง€, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ rc 0(๊ฑฐ๋ถ€ ๋กœ๊ทธ) | | T1-4 | `MQTT_HOST` ๋ถˆ๊ฐ€ + ๋””์Šคํฌ ๋น„์ข…๋ฃŒ (`--timeout 8`) | rc=3, ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ์žฌ๊ด€์ธก ๋กœ๊ทธ, ์ตœ์ข… `error` | | T1-5 | rc=3 + ์žฌ๊ด€์ธก ์ค‘ ๋””์Šคํฌ `completed` ์ถœํ˜„ | rc=0, `completed` | | T1-6 | `close --request deadline_timeout`์„ `completed` ๋ ˆ์ฝ”๋“œ์— ์ ์šฉ | rc=0, ์ƒํƒœ ๋ถˆ๋ณ€ | | T1-10 | loop: ์›Œ์ปค `completed` ํ›„ ๋ฆฌ๋ทฐ์–ด `update --status pending --iteration 1 --attempt-id 2` | rc=0, `pending`, `attempt_id=2` (์ˆ˜์ • ์ „ rc=4 โ†’ ์—ญ๊ฒ€์ฆ) | | T1-11 | ๊ฐ™์€ `--attempt-id 2` ์žฌ์š”์ฒญ(๋ฆฌํ”Œ๋ ˆ์ด) | rc=4 ๊ฑฐ๋ถ€ | | T1-12 | `--attempt-id` ์—†์ด iteration 2 (F-A ๊ฒฝ๋กœ) | rc=0 (๊ธฐ์กด ๊ทœ์น™ ๋ณด์กด), F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | | T1-13 | `close --request cancelled --detail session_busy`: pendingโ†’cancelled rc=0, completedโ†’ rc=0ยท์ƒํƒœ ๋ถˆ๋ณ€, ์ดํ›„ `redispatch` iteration 2 โ†’ pending | ์ „์ด ๊ทœ์น™ ๊ณ ์ • | | T1-7 | ์ปค์Šคํ…€ `--registry-dir` + `send_keys_safe` ์‹คํŒจ(BASH_ENV ์˜ค๋ฒ„๋ผ์ด๋“œ) | rc=1, ํ•ด๋‹น ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error` | | T1-8 | `MAM_DELEGATE_FAIL_CLOSED=0` | T1-1 ์กฐ๊ฑด์—์„œ rc=0, `pending` (๊ตฌ ๋™์ž‘ ์žฌํ˜„) | | T1-9 | dry-run direct | ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค ๋ฏธ๊ธฐ๋™ | | ๊ณ ์ • | `test_wrapper_exit_code_mapping_literals_are_frozen` | ํ†ต๊ณผ | ### 5.2 `tests/test_issue6_turn_handshake.py` (mock_herdr ์ƒํƒœ ์‹œํ€€์Šค) | ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | |---|---|---| | T2-1 | `status_sequence=["working","done"]` | `agent wait` ํ˜ธ์ถœ ํ›„ `agent prompt` ํ˜ธ์ถœ(์ˆœ์„œ ๋‹จ์ •), Enter/C-m 0ํšŒ(๋„ค์ดํ‹ฐ๋ธŒ) | | T2-2 | `working` ๊ณ ์ •, `SKS_IDLE_TIMEOUT_MS=1000` | rc=1, `agent prompt` ๋ฏธํ˜ธ์ถœ, ๋ž˜ํผ rc=1ยท`error` | | T2-3 | `unknown` | `agent wait` ๋ฏธํ˜ธ์ถœ, `_pane_quiescent` ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… | | T2-4 | `blocked` | rc=2, ์ฃผ์ž… ์—†์Œ | | T2-5 | `done` ์ฆ‰์‹œ | ๋Œ€๊ธฐ 0ํšŒ, ์ฆ‰์‹œ ์ฃผ์ž… | | T2-6 | ํด๋ฐฑ ๊ฒฝ๋กœ, pre_submit์— `esc to interrupt` ์กด์žฌ + ํŽ˜์ด์ŠคํŠธ ๋“œ๋กญ | rc=4 (๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” 0 โ†’ ์—ญ๊ฒ€์ฆ) | | T2-7 | `prompt_wait_result=agent_prompt_stalled` + ์ดํ›„ `working` | rc=0, prompt ํ˜ธ์ถœ **1ํšŒ** | | T2-8 | ๋ž˜ํผ ์ˆœ์„œ: idle ๋Œ€๊ธฐ ๋กœ๊ทธ๊ฐ€ `subscriber pid:` ๋กœ๊ทธ๋ณด๋‹ค ์•ž | ๋กœ๊ทธ ์ˆœ์„œ ๋‹จ์ • | | T2-9 | `working` ๊ณ ์ • + `SKS_IDLE_RETRIES=1`, `SKS_IDLE_TIMEOUT_MS=500` | `agent wait` 2ํšŒ ํ˜ธ์ถœ, ๋ž˜ํผ rc=5, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `cancelled`(detail `session_busy`), `deadline_timeout` ๋ฏธํ˜ธ์ถœ, ๊ตฌ๋…์ž ๋ฏธ๊ธฐ๋™ | | T2-10 | `HERDR_SESSION_NAME=iso-x`์—์„œ `wait_for_agent_idle` | mock ํ˜ธ์ถœ ๊ธฐ๋ก์— `herdr --session iso-x agent wait โ€ฆ` (argv ์ˆœ์„œ ๋‹จ์ •); `build_argv(session="default")`๋Š” `--session` ์ƒ๋žต | | T2-11 | ์ •์  ๋ฆฐํŠธ: `lib.sh`ยท๋ž˜ํผ์—์„œ `^\s*python3? -m lib_py\.loop` ๋˜๋Š” bare `python -m` ํŒจํ„ด 0๊ฑด, watcher ํ˜ธ์ถœ์€ `_delegate_py_bin` ๊ฒฝ์œ  | grep ๋‹จ์ • | | ํ˜ธํ™˜ | `test_h16`, `test_b8`, `test_c1_*`, `test_herdr_shim_contract` | ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | ### 5.3 Phase 3 (`tests/test_issue6_fail_closed.py` ๋‚ด) | ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | |---|---|---| | T3-1 | `LoopRunner.delegate_job` rcโ‰ 0 + `registered job:` ์ถœ๋ ฅ | id ๋ฐ˜ํ™˜, `wait_for_job` โ‰ค 2s False | | T3-2 | ๋™์ผ | `.mam/loop-checkpoint.json`์— id ๊ธฐ๋ก | | T3-3 | ๋ž˜ํผ stub์ด 1ํšŒ์ฐจ rc=5, 2ํšŒ์ฐจ rc=0 (`MAM_SESSION_BUSY_RETRY_S=0`) | ์žฌ์œ„์ž„ 1ํšŒ ํ›„ 2ํšŒ์ฐจ id ๋ฐ˜ํ™˜; ๋‘ ๋ฒˆ ๋‹ค rc=5๋ฉด ๋งˆ์ง€๋ง‰ id ๋ฐ˜ํ™˜ + `wait_for_job` False | --- ## 6. ๊ณต์ˆ˜ ๋ฐ ์ˆœ์„œ | ๋‹จ๊ณ„ | ๊ณต์ˆ˜ | ์„ ํ–‰ | |---|---|---| | Phase 1 | 1.25 MD (+0.25 attempt_id) | โ€” | | Phase 2 | 1.25โ€“1.75 MD (+0.25 ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„) | Phase 1 (finalize ํ—ฌํผ ์žฌ์‚ฌ์šฉ) | | Phase 3 | 0.25 MD | Phase 1 | | Phase 4 | 0.75 MD (Phase 1~3์™€ ๋ณ‘ํ–‰ ์ž‘์„ฑ, ์—ญ๊ฒ€์ฆ ํฌํ•จ) | โ€” | | Phase 5 | 0.5โ€“1.0 MD | ์ „๋ถ€ | | **ํ•ฉ๊ณ„** | **4.0โ€“5.0 MD** (ํ†ตํ•ฉ ๋ณด๊ณ ์„œ Rev.2 ยง3 ์ƒํ•œ ๋Œ€๋น„ +0.5 MD; ์‚ฌ์œ  ยง7) | | --- ## 7. Creator ์ด์˜์ œ๊ธฐ ํŒ์ • (Rev.1 โ†’ Rev.2) | ์ด์˜ | ํŒ์ • | ์‹ค์ธก ๊ทผ๊ฑฐ | Rev.2 ๋ฐ˜์˜ | |---|---|---|---| | **Primary-A** watcher.py๊ฐ€ `HERDR_SESSION_NAME` ๊ฒฉ๋ฆฌ๋ฅผ ์žƒ๋Š”๋‹ค | **์ˆ˜์šฉ** (์„ธ๋ถ€ ์ •์ •: ์‹ค์ œ herdr ํ”Œ๋ž˜๊ทธ๋Š” `-L`์ด ์•„๋‹ˆ๋ผ `--session`; `-L`์€ `.mam/shim` ๋ฒˆ์—ญ ์ „์šฉ. ๊ฒฉ๋ฆฌ ๋ณ€์ˆ˜๋Š” ์…ธ ํ•จ์ˆ˜ `mam_herdr`/`_real_herdr`๊ฐ€ ๋ฒˆ์—ญํ•˜๋ฉฐ Python ์„œ๋ธŒํ”„๋กœ์„ธ์Šค์—๋Š” ์ƒ์†๋˜์ง€ ์•Š์Œ) | `HERDR_SESSION_NAME`๋งŒ ์žˆ๋Š” ํ™˜๊ฒฝ์—์„œ bare `herdr` โ†’ `server_not_running`; `--session`/`HERDR_SESSION`/`HERDR_SOCKET_PATH` โ†’ ์ •์ƒ. `lib.sh` source ํ›„ Python `subprocess`๋„ ๋™์ผ ์‹คํŒจ | ยง1 ์›์น™ 8, ยง3.2.1 `--session` ์ „๋‹ฌ, ยง3.2.2 `_sks_herdr` ์‚ฌ์šฉ, T2-10 | | **Primary-B** bare `python -m` ํ˜ธ์ถœ์€ `ModuleNotFoundError` | **์ˆ˜์šฉ** (Rev.1 ํ‘œ๊ธฐ ์˜ค๋ฅ˜) | `lib.sh:1841 _delegate_py_bin`, `PYTHONPATH` export `lib.sh:25,164` | ยง3.2.1/3.2.2 `"$(_delegate_py_bin)"`, T2-11 ์ •์  ๋ฆฐํŠธ | | **Secondary** ์ฃผ์ž… ์ „ ์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋ฅผ `deadline_timeout`(terminal `error`)๋กœ ๋‹ซ์œผ๋ฉด ๋ฏธ์ฃผ์ž… ์žก์ด ์‹คํ–‰ ์‹คํŒจ๋กœ ๋‚™์ธ + ๋Ÿฌ๋„ˆ ์ฆ‰์‹œ abort | **์ˆ˜์šฉ** (๋Œ€์•ˆ ์ฑ„ํƒ: ๋ž˜ํผ ๋‚ด bounded ์žฌ์‹œ๋„ โ†’ `cancelled`/`session_busy` rc 5 โ†’ ๋Ÿฌ๋„ˆ 1ํšŒ ์žฌ์œ„์ž„). "์žฌ์‹œ๋„ ๊ธฐํšŒ ์˜๊ตฌ ๋ฐ•ํƒˆ" ๋ถ€๋ถ„์€ ๋ถ€๋ถ„ ์ •์ •: `cancelled`๋„ operator `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ(์‹ค์ธก) | `engine.transition`: pendingโ†’cancelled ์ปค๋ฐ‹, completedโ†’cancelled ๊ฑฐ๋ถ€, cancelledโ†’redispatch ์„ฑ๊ณต | ยง1 ์›์น™ 7, ยง3.1.1 `close`, ยง3.1.2/3.1.4 rc 5, ยง3.2.2 ์žฌ์‹œ๋„, ยง3.3 ๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„, T1-13/T2-9/T3-3 | | **Tertiary** ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜ ์‹œ ๊ฐ™์€ iteration์˜ `redispatch` ๊ฑฐ๋ถ€ | **์ˆ˜์šฉ** (์ธ์šฉ ์ฝ”๋“œ๋Š” ์‹ค์ œ `engine.py:227-240`๊ณผ ๋‹ค๋ฅด๋‚˜ ๊ฒฐ๋ก ์€ ์ •ํ™•). v4.4.0์—์„œ **์ด๋ฏธ ๊นจ์ ธ ์žˆ๋Š” ๊ธฐ์กด ๊ฒฐํ•จ**์ž„์„ ํ™•์ธ | `registry.py update --status pending --iteration 1`(์›Œ์ปค completed ํ›„) โ†’ rc=4 `does not advance past 1`; iteration 2 โ†’ rc=0 | ยง3.1.6 `attempt_id` ๊ทœ์•ฝ, T1-10~T1-12 | | (๋ถ€์ˆ˜) Rev.2 ํ†ตํ•ฉ ๋ณด๊ณ ์„œ ยง3์˜ `lib.sh` ๊ฒฝ๋กœ | Rev.1๊ณผ ๋™์ผํ•˜๊ฒŒ ์‹ค์ œ ๊ฒฝ๋กœ `.agents/skills/lib.sh` ์‚ฌ์šฉ | โ€” | โ€” | ๊ณต์ˆ˜ +0.5 MD์˜ ์‚ฌ์œ : `attempt_id` ๊ทœ์•ฝ(์—”์ง„ ๊ทœ์น™ยทCLIยทpublish_event ๊ธฐ๋ณธ๊ฐ’ยทํ…Œ์ŠคํŠธ 3์ข…)๊ณผ ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„ ๋กœ์งยท๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„(ํ…Œ์ŠคํŠธ 3์ข…)์ด Rev.1์— ์—†๋˜ ์‹ ๊ทœ ์ž‘์—…์ด๋‹ค.