# ๐Ÿ“‘ Issue #5 Phase 2a โ€” Shadow Watcher ์šด์˜ ๋…ธํŠธ - **๋ฌธ์„œ ๋ฒ„์ „**: Rev.1 - **์ƒํƒœ**: Phase 2a ์™„๋ฃŒ (**shadow-only**: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฌด๊ธฐ๋ก) --- ## 1. ์ด ๋‹จ๊ณ„๊ฐ€ ํ•˜๋Š” ์ผ๊ณผ ํ•˜์ง€ ์•Š๋Š” ์ผ | ํ•œ๋‹ค | ํ•˜์ง€ ์•Š๋Š”๋‹ค | |---|---| | `herdr agent wait`๋กœ ์—์ด์ „ํŠธ ์ƒํƒœ๋ฅผ ๊ด€์ธก | ๊ด€์ธก์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ์ผ | | ์ฆ๊ฑฐ๋ฅผ ์ •์ฑ…์— ๋„ฃ์–ด "์™„๋ฃŒ ํ›„๋ณด์ธ๊ฐ€"๋ฅผ **ํŒ์ •** | ํŒ์ • ๊ฒฐ๊ณผ๋ฅผ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— **์ปค๋ฐ‹** | | ํŒ์ •์„ `.mam/shadow/.ndjson`์— ๊ธฐ๋ก | job ๋ ˆ์ฝ”๋“œ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€/๋ณ€๊ฒฝ | | 4์ค‘ ๊ท€์† ์ฆ๊ฑฐ๋ฅผ shadow ์ €์žฅ์†Œ์— ๋ณด๊ด€ | ๋ ˆ์ฝ”๋“œ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ (Phase 2b ๊ฒฐ์ •) | **์ž๋™์™„๋ฃŒ๋Š” Phase 2b๋‹ค.** ๋ณธ ๋‹จ๊ณ„์˜ `candidate_approved`๋Š” "์ปค๋ฐ‹ํ–ˆ์„ ๊ฒƒ"์ด๋ผ๋Š” ๋กœ๊ทธ์ผ ๋ฟ์ด๋‹ค. ## 2. ์šด์˜ ์Šค์œ„์น˜ | ๋ณ€์ˆ˜ | ๊ธฐ๋ณธ | ํšจ๊ณผ | |---|---|---| | `MAM_ENGINE_CAS` | `1` | `0`์ด๋ฉด `update_job_status`/`pick_pending`์ด Phase 2a ์ด์ „์˜ ๋ณด์กด๋œ ๋ณธ์ฒด๋กœ ๋ถ„๊ธฐํ•œ๋‹ค. ์žฌ๋ฐฐํฌยทrevert ๋ถˆํ•„์š”. | Shadow ๋Ÿฌ๋„ˆ๋Š” ์• ์ดˆ์— ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์—†์œผ๋ฏ€๋กœ **ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ๋งŒ์œผ๋กœ ํ”์ ์ด ์‚ฌ๋ผ์ง„๋‹ค.** `.mam/shadow/`๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ์‚ญ์ œํ•ด๋„ job ์ƒํƒœ์— ์˜ํ–ฅ์ด ์—†๋‹ค. ## 2-1. ๊ถŒํ•œ ๊ตฌ๋ถ„ (๊ต์ฐจ ๋ฆฌ๋ทฐ ๋ฐ˜์˜) | source | ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ | ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ | |---|---|---| | `publish_event` (์—์ด์ „ํŠธ) | ์ž์‹ ์˜ ์ž‘์—…์„ `running`/`completed`/`error`/`cancelled`๋กœ ์ „์ด | **terminal์„ ๋˜๋Œ๋ฆฌ๋Š” ๊ฒƒ** (`redispatch`/`finalize` ๊ฑฐ๋ถ€) | | `registry_cli` (๋ž˜ํผยท์šด์˜์ž) | ์œ„ ์ „๋ถ€ + `redispatch`(terminalโ†’`pending`, iteration ์ฆ๊ฐ€ ํ•„์ˆ˜) + `finalize`(terminalโ†’`error`) | iteration์„ ๋˜๋Œ๋ฆฌ๋Š” ์žฌ์ƒ | **CLI ์ข…๋ฃŒ ์ฝ”๋“œ**: `registry.py status`/`update`๋Š” **์ง„์งœ ๊ฑฐ๋ถ€**์—๋งŒ non-zero(4)๋ฅผ ๋‚ธ๋‹ค. ์ž˜๋ชป๋œ status ๊ฐ’์€ rc 1(2a ์ด์ „ ๊ณ„์•ฝ ์œ ์ง€), **๋ฉฑ๋“ฑ ACK(์ด๋ฏธ ๊ทธ ์ƒํƒœ์ž„)๋Š” rc 0**์ด๋‹ค โ€” ๋ž˜ํผ๊ฐ€ `set -e` ์•„๋ž˜์—์„œ ์Šน์ธ ์งํ›„ `--set completed`๋ฅผ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ์ด๋ฅผ ์‹คํŒจ๋กœ ๋ณด๊ณ ํ•˜๋ฉด ์„ฑ๊ณต ๊ฒฝ๋กœ๊ฐ€ ์ค‘๋‹จ๋œ๋‹ค(๊ต์ฐจ ๋ฆฌ๋ทฐ F-C/F-D). Phase 2a ์ด์ „์—๋Š” ๊ฑฐ๋ถ€๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ด ํ•ญ์ƒ 0์ด์—ˆ๊ณ , ๊ทธ ์นจ๋ฌต์ด ๋ฐ˜๋ณต ์œ„์ž„์ด ์กฐ์šฉํžˆ ๊นจ์ง„ ๊ฒƒ์„ ๊ฐ์ท„๋‹ค. ## 3. ๋ฝ ๊ทœ์œจ (P2A-01) ``` with registry_lock: ํŒ์ • + ๋ ˆ์ฝ”๋“œ 1ํšŒ ์“ฐ๊ธฐ + ์•Œ๋ฆผ ํ์ž‰ โ† ๋ธ”๋กœํ‚น ํ˜ธ์ถœ ๊ธˆ์ง€ ๋ฝ ํ•ด์ œ ํ›„: ๊ฐ์‚ฌ ๋กœ๊ทธ flush, ๋ฐœํ–‰ ๋“ฑ ๋А๋ฆฐ ์ž‘์—… ``` ๊ฐ์‚ฌ mirror๊ฐ€ ๋ฝ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด์„œ ์ˆœ์„œ ๋ณด์žฅ์„ ์žƒ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ „์ด๋งˆ๋‹ค ๋‹จ์กฐ ์ฆ๊ฐ€ํ•˜๋Š” `transition_seq`๋ฅผ ๋ ˆ์ฝ”๋“œ์™€ ์•Œ๋ฆผ์— ํ•จ๊ป˜ ์‹ฃ๋Š”๋‹ค. **`update_logged_status`๋Š” ์ „์šฉ per-job ๊ฐ์‚ฌ ๋ฝ ์•„๋ž˜์—์„œ ์ฝ๊ธฐ-๋น„๊ต-์“ฐ๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ , ๊ธฐ์กด `status.json`์˜ `transition_seq`๊ฐ€ ๋” ํฌ๊ฑฐ๋‚˜ ๊ฐ™์œผ๋ฉด ์“ฐ๊ธฐ๋ฅผ ๊ฑด๋„ˆ๋›ด๋‹ค**(๊ต์ฐจ ๋ฆฌ๋ทฐ R4: ๋ฝ ์—†๋Š” ๋น„๊ต-ํ›„-์“ฐ๊ธฐ๋Š” ๋‘ flush๊ฐ€ ๋ชจ๋‘ ์˜› ๊ฐ’์„ ์ฝ๊ณ  ๋А๋ฆฐ ์ชฝ์ด ์ตœ์‹ ์„ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์–ด, ์‹œํ€€์Šค ๋ฒˆํ˜ธ๋งŒ์œผ๋กœ๋Š” ์ˆœ์„œ๊ฐ€ ๊ฐ•์ œ๋˜์ง€ ์•Š๋Š”๋‹ค). ์ด ๋ฝ์€ **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํŠธ๋žœ์žญ์…˜ ๋ฐ–**์—์„œ ์žก์œผ๋ฏ€๋กœ ๋А๋ฆฐ ๊ฐ์‚ฌ I/O๊ฐ€ ํŠธ๋žœ์žญ์…˜์„ ๋Š˜๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. ๊ฐ ์“ฐ๊ธฐ๋Š” ๊ณ ์œ  ์ž„์‹œ ํŒŒ์ผ์„ ์“ด๋‹ค โ€” ์ง€์—ฐ๋œ flush๊ฐ€ ์ตœ์‹  ์ƒํƒœ๋ฅผ ๋˜๋Œ๋ฆฌ์ง€ ๋ชปํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋ฉฐ, ๋ฝ์ด ๊ณต์งœ๋กœ ์ฃผ๋˜ ์ˆœ์ฐจ ์ผ๊ด€์„ฑ์„ ๋ณต์›ํ•œ๋‹ค(๊ต์ฐจ ๋ฆฌ๋ทฐ F-B). ์ด ํ˜•ํƒœ๋Š” ์ƒˆ๋กœ ๋งŒ๋“  ๊ฒƒ์ด ์•„๋‹ˆ๋ผ `pick_pending`์ด ์ด๋ฏธ ์“ฐ๋˜ ํŒจํ„ด์„ ๋ชจ๋“  ์ „์ด๋กœ ์ผ๋ฐ˜ํ™”ํ•œ ๊ฒƒ์ด๋‹ค. ## 4. `herdr agent wait` ์‚ฌ์šฉ ๊ทœ์น™ - `--timeout`์€ **๋ฐ€๋ฆฌ์ดˆ**๋‹ค. ์ฝ”๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…์€ `timeout_ms`์ด๋ฉฐ ์ดˆ ๋‹จ์œ„ ํ˜•ํƒœ๋Š” ์ œ๊ณตํ•˜์ง€ ์•Š๋Š”๋‹ค. - `--until`์€ **ํ•ญ์ƒ ๋ช…์‹œ**ํ•œ๋‹ค. ์ƒ๋žต ์‹œ ๊ธฐ๋ณธ ๋งค์นญ์ด `idle,done,blocked`๋ผ `working`์ด ๋น ์ง„๋‹ค. - `exit 1`์˜ ํŽ˜์ด๋กœ๋“œ๋Š” **stderr**์— ์žˆ๋‹ค. stdout์€ ๋น„์–ด ์žˆ์œผ๋ฏ€๋กœ stdout์—์„œ ์—๋Ÿฌ๋ฅผ ์ฝ์œผ๋ ค ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. - **ํƒ€์ž„์•„์›ƒ์€ ๊ฑด๊ฐ•ํ•œ ์‘๋‹ต์ด๋‹ค.** ์†Œ์ผ“์ด ๋‹ตํ–ˆ๊ณ  ์ƒํƒœ๊ฐ€ ์•ˆ ๋ฐ”๋€ ๊ฒƒ๋ฟ์ด๋‹ค. ์ „์†ก ์‹คํŒจ์™€ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•œ๋‹ค. - ์ „์†ก ์‹คํŒจ ์‹œ ๋ฐฑ์˜คํ”„๋Š” 1โ†’2โ†’4โ†’8โ†’16์ดˆ ์ƒํ•œ์ด๋ฉฐ, **์บ์‹œ๋œ ์Šค๋ƒ…์ƒท์„ ํ๊ธฐ**ํ•œ๋‹ค. ๋‹จ์ ˆ ๊ตฌ๊ฐ„ ์ด์ „์˜ ์ƒํƒœ๋กœ ์ถ”๋ก ํ•˜๋ฉด ๋‹จ์ ˆ์ด ์™„๋ฃŒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค. ## 4-1. ์™„๋ฃŒ ๊ท€์†์— ์š”๊ตฌ๋˜๋Š” ์ฆ๊ฑฐ (๊ต์ฐจ ๋ฆฌ๋ทฐ R1) `candidate_approved`๋Š” **์ฆ๊ฑฐ์˜ ์กด์žฌ**๋ฅผ ์š”๊ตฌํ•œ๋‹ค. ๊ฐ’์ด ์žˆ์„ ๋•Œ๋งŒ ๋น„๊ตํ•˜๋Š” ๋ฐฉ์‹์€, ์•„๋ฌด ์‹๋ณ„์ž๋„ ์—†๋Š” ์ฆ๊ฑฐ๋ฅผ dispatch ๋ ˆ์ฝ”๋“œ์˜ ์ž๊ธฐ ์„ ์–ธ๋งŒ์œผ๋กœ ์Šน์ธํ•ด ๋ฒ„๋ฆฐ๋‹ค โ€” ์ด ๊ฒ€์‚ฌ๊ฐ€ ๋Œ€์ฒดํ•˜๋ ค๋˜ ๋ฐ”๋กœ ๊ทธ ์ž๊ธฐ ์„ ์–ธ์ด๋‹ค. | ์š”๊ตฌ ์ฆ๊ฑฐ | ์œ„์น˜ | ์—†์œผ๋ฉด | |---|---|---| | `job_id`, `attempt_id`, `agent_session_uuid`, `pane_process_generation` | ํƒ€์ž„๋ผ์ธ ํ•ญ๋ชฉ์˜ **`collector_attribution` ๋ด‰ํˆฌ** (๋˜๋Š” event/filesystem ๋“ฑ **๋น„-herdr** ์†Œ์Šค์˜ payload) | `hold` | | ๋ฆฌํฌํŠธ ๋งค๋‹ˆํŽ˜์ŠคํŠธ์˜ `attempt_id` | filesystem ์†Œ์Šค payload์˜ `report_manifest` | `hold` | **๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๊ณ„ ์œ ์ง€**: ์‹๋ณ„์ž๋Š” **๋„ค์ดํ‹ฐ๋ธŒ Herdr payload ์•ˆ์—์„œ ์ฝ์ง€ ์•Š๋Š”๋‹ค.** Phase 0์ด ๋„ค์ดํ‹ฐ๋ธŒ ํŽ˜์ด๋กœ๋“œ์— MAM ํ•„๋“œ ์ฃผ์ž…์„ ๊ธˆ์ง€ํ•˜๋ฏ€๋กœ, ๊ฑฐ๊ธฐ ๋‚˜ํƒ€๋‚œ ์‹๋ณ„์ž๋Š” ์กฐ์ž‘์ด๊ฑฐ๋‚˜ ๊ณ„์•ฝ ์œ„๋ฐ˜์ด๋ฉฐ ์–ด๋А ์ชฝ์ด๋“  ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ˆ˜์ง‘๊ธฐ์˜ ์—ฐ๊ด€ ์ •๋ณด๋Š” payload **์˜†**์˜ ๋ด‰ํˆฌ์— ๋‘”๋‹ค. ## 5. ํŒ์ • ์ˆœ์„œ (fail-closed) 1. ์ „์†ก ์ด์ƒ โ†’ `hold_reconnect` (์ฆ๊ฑฐ ์ž์ฒด๊ฐ€ ๋ฌดํšจ) 2. ๋‹ค๋ฅธ ์„ธ๋Œ€/๋‹ค๋ฅธ pane ์‹ ํ˜ธ โ†’ `reject_stale` 3. `blocked` โ†’ `hold_blocked` (์‚ฌ๋žŒ ๋Œ€๊ธฐ ์ค‘ ์™„๋ฃŒ ๊ธˆ์ง€) 4. 4์ค‘ ๊ท€์† + ์‹คํ–‰ ์•ต์ปค ๋ฏธ์ถฉ์กฑ โ†’ `hold` 5. ๋น„-quiescent ๋˜๋Š” ๋„๊ตฌ ์‹คํ–‰ ์ค‘ โ†’ `hold` 6. ๋ฆฌํฌํŠธ ๋ฏธํ™•์ •(๋งค๋‹ˆํŽ˜์ŠคํŠธ/ํ•ด์‹œ) โ†’ `hold` 7. ์ „๋ถ€ ํ†ต๊ณผ โ†’ `candidate_approved` ## 6. ์‹œ๋‚˜๋ฆฌ์˜ค ๊ณ„์•ฝ Phase 0์˜ 7์ข… + Phase 2a์˜ 5์ข… = **12์ข…**์ด `expected_policy.decision`์œผ๋กœ ํŒ์ •์„ ๊ณ ์ •ํ•œ๋‹ค. ์‹ ๊ทœ 5์ข…๋„ Phase 0์˜ `validate_watcher_scenario`๋ฅผ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ฆ๊ฑฐ ์Šคํ‚ค๋งˆ๊ฐ€ ๊ณ„์† ๊ฐ•์ œ๋œ๋‹ค. ## 7. Phase 2b ์ธ๊ณ„ 1. `attempt_id` ๋“ฑ ๊ท€์† ํ•„๋“œ๋ฅผ ๋ ˆ์ฝ”๋“œ๋กœ ์Šน๊ฒฉํ• ์ง€ ๊ฒฐ์ • (ํ˜„์žฌ๋Š” shadow ์ €์žฅ์†Œ). 2. `policy.decide`์˜ `candidate_approved`๋ฅผ `engine.transition("completed", expected_attempt=...)`์— ์—ฐ๊ฒฐ. ์—”์ง„๊ณผ CAS ํ‘œ๋Š” ์ด๋ฏธ ์ค€๋น„๋˜์–ด ์žˆ๋‹ค. 3. Pending Notification ์ง€์—ฐ ์•Œ๋ฆผยทํฌ๋ž˜์‹œ ๋ณต๊ตฌ (`state-transitions.md` ยง5). 4. `preexec_fn`: Phase 2a๋Š” ๋ฐ๋ชฌ์„ ์Šคํฐํ•˜์ง€ ์•Š์•„ ๋ฏธ๋…ธ์ถœ. 2b๊ฐ€ ์Šคํฐ์„ ๋„์ž…ํ•˜๋ฉด ์Šค๋ ˆ๋“œ ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ต์ฒดํ•ด์•ผ ํ•œ๋‹ค. 5. **`started`์˜ ์„ธ์…˜ ํ™•์ธ (O-A)**: `_decide`์˜ `claim`/`started`๋Š” ์•„์ง "์ž‘์—… ํ• ๋‹น ์„ธ์…˜ ์ผ์น˜"๋ฅผ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š๋Š”๋‹ค(`pick_pending`์€ ์„ธ์…˜ ํ•„ํ„ฐ๋ฅผ ํ•˜์ง€๋งŒ `publish_event`์˜ `started`๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค). 2b์—์„œ `expected_session`์„ ์ถ”๊ฐ€ํ•  ๊ฒƒ. 6. **`stop_session.sh`์˜ ๋ฌด์กฐ๊ฑด `cancelled` (O-B)**: ์ด๋ฏธ `completed`์ธ job์— ๋Œ€ํ•ด ์ด์ œ ๊ฑฐ๋ถ€๋œ๋‹ค(๋ ˆ๊ฑฐ์‹œ๋Š” ๋ฎ์–ด์ผ๋‹ค). ํ‘œ์ƒ ์˜๋„๋œ ๋™์ž‘์ด๋ฉฐ, ์šด์˜์ž๊ฐ€ ํ™•์ • ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋ ค๋ฉด `registry.py status --set`(์šด์˜์ž ๊ถŒํ•œ)์„ ์“ด๋‹ค. ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ ๊ธฐ๋ก ๋Œ€์ƒ.