diff --git a/.agents/reports/creator-claude-01/plan-2770df36.md b/.agents/reports/creator-claude-01/plan-2770df36.md new file mode 100644 index 0000000..50fa734 --- /dev/null +++ b/.agents/reports/creator-claude-01/plan-2770df36.md @@ -0,0 +1,364 @@ +# ๐Ÿ“‘ Issue #5 Phase 2a ๊ตฌํ˜„ ๊ณ„ํš์„œ โ€” `lib_py/loop/` ์ฝ”์–ด & Shadow Watcher + +- **Job**: `2770df36` / **Planner**: `creator-claude-01` +- **๊ธฐ์ค€ ์ปค๋ฐ‹**: `015ab70` (branch `refactor`, working tree clean) / Python `3.9.6` / paho-mqtt `2.1.0` / herdr `0.8.2` / **์ „์ฒด ํ…Œ์ŠคํŠธ ์ˆ˜์ง‘ 576** +- **์š”๊ตฌ ์ถœ์ฒ˜**: `implementation_plan.md` ยง2 Phase 2a ํ–‰, `task.md` P2A-01~P2A-04, `state-transitions.md` ยง2~ยง5 +- **๋ฌธ์„œ ์„ฑ๊ฒฉ**: Worker๊ฐ€ ๊ทธ๋Œ€๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋‹จ๊ณ„๋ณ„ ๋ช…์„ธ. ยง1์˜ ๋ชจ๋“  ์ธ์šฉ์€ ๊ณ„ํš ์ˆ˜๋ฆฝ ์ค‘ **๋ผ์ด๋ธŒ ์ฝ”๋“œยท๋ผ์ด๋ธŒ CLI๋กœ ์ง์ ‘ ํ™•์ธ**ํ–ˆ๋‹ค. + +--- + +## 0. ์š”์•ฝ (TL;DR) + +1. **Phase 2a๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋‹จ 1๋ฐ”์ดํŠธ๋„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.** Shadow watcher๋Š” "Would-Complete" ํŒ์ •์„ ๋กœ๊ทธ๋กœ๋งŒ ๋‚จ๊ธด๋‹ค. ์ด ๋ถˆ๋ณ€์‹์„ ๋ฌธ์žฅ์ด ์•„๋‹ˆ๋ผ **๊ตฌ์กฐ์™€ ํ…Œ์ŠคํŠธ๋กœ** ๊ฐ•์ œํ•œ๋‹ค(ยงD5, G4). +2. **CAS ๊ฒฝ๊ณ„๋Š” ์‹ ๊ทœ ํŒŒ์ผ์ด ์•„๋‹ˆ๋ผ ๊ธฐ์กด writer 3๊ณณ์„ ํ•œ ํ•จ์ˆ˜ ๋’ค๋กœ ๋ชจ์œผ๋Š” ์ž‘์—…์ด๋‹ค.** ํ˜„์žฌ ์ƒํƒœ writer๋Š” 3๊ฐœ์ด๊ณ  ๊ทธ์ค‘ **ํ•˜๋‚˜(`pick_pending`)๋Š” ์ด๋ฏธ ๋ฌธ์„œํ™”๋œ chokepoint๋ฅผ ์šฐํšŒ**ํ•œ๋‹ค(ยง1-2 ์‹ค์ธก). +3. **4์ค‘ ๊ท€์†์— ํ•„์š”ํ•œ ํ•„๋“œ๊ฐ€ job ๋ ˆ์ฝ”๋“œ์— ์—†๋‹ค**(`attempt_id`/`agent_session_uuid`/`pane_process_generation` ์ „๋ถ€ ๋ถ€์žฌ, ยง1-4). Phase 1์ด ๋ ˆ์ฝ”๋“œ ์Šคํ‚ค๋งˆ ๋ถˆ๋ณ€์„ ์•ฝ์†ํ–ˆ๊ณ  Phase 2a๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์“ฐ๊ธฐ๊ฐ€ ๊ธˆ์ง€๋˜๋ฏ€๋กœ, **๊ท€์† ์ฆ๊ฑฐ๋Š” shadow ์ „์šฉ ์ €์žฅ์†Œ์— ๋‘”๋‹ค.** ๋ ˆ์ฝ”๋“œ ์Šคํ‚ค๋งˆ ๊ฒฐ์ •์€ Phase 2b๋กœ ๋ฏธ๋ฃฌ๋‹ค. +4. **`herdr agent wait --timeout`์˜ ๋‹จ์œ„๋Š” ๋ฐ€๋ฆฌ์ดˆ๋‹ค**(๋ผ์ด๋ธŒ `--help` ํ™•์ธ, Phase 0 fixture๋„ `integer_ms`๋กœ ์ •ํ™•ํžˆ ๊ธฐ๋ก). ์ดˆ ๋‹จ์œ„๋กœ ์˜คํ•ดํ•˜๋ฉด 1000๋ฐฐ ์˜ค์ฐจ๊ฐ€ ๋‚œ๋‹ค. +5. **๋ฝ ์•ˆ์—์„œ ๊ฐ์‚ฌ ๋กœ๊ทธ I/O๊ฐ€ ์ผ์–ด๋‚˜๊ณ  ์žˆ๋‹ค**(`update_job_status`๊ฐ€ ๋ฝ ๋ณด์œ  ์ค‘ `update_logged_status`/`append_event` ํ˜ธ์ถœ, ยง1-3). Phase 2a๋Š” ์ด๋ฅผ "๋ฝ ์•ˆ์—์„œ ๊ฒฐ์ • + ์•Œ๋ฆผ ํ์ž‰ โ†’ ๋ฝ ํ•ด์ œ โ†’ ๋ฐ–์—์„œ flush"๋กœ ๋ฐ”๊พผ๋‹ค. +6. **ํŒ์ • ๋กœ์ง์˜ ํ…Œ์ŠคํŠธ ๋ฒกํ„ฐ๋Š” ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.** Phase 0์—์„œ 3์ธ ๋ฆฌ๋ทฐ๋กœ ๊ฒ€์ฆ๋œ watcher ์‹œ๋‚˜๋ฆฌ์˜ค 7์ข…๊ณผ ๊ทธ `expected_policy`๊ฐ€ ๊ทธ๋Œ€๋กœ ์—”์ง„์˜ ๊ฒฐ์ • ํ…Œ์ด๋ธ” ํ…Œ์ŠคํŠธ๊ฐ€ ๋œ๋‹ค(ยงD7). + +--- + +## 1. Planner ์‚ฌ์ „ ์‹ค์ธก + +### 1-1. ๋Œ€์ƒ ํŒŒ์ผ๊ณผ ๊ทœ๋ชจ + +| ํ•ญ๋ชฉ | ์‹ค์ธก | +|---|---| +| ๊ธฐ์ค€ ์ปค๋ฐ‹ | `015ab70` | +| ์ „์ฒด ํ…Œ์ŠคํŠธ | **576 ์ˆ˜์ง‘** (Phase 1 ์ข…๋ฃŒ ์‹œ์ ) | +| `lib_py/` ๊ธฐ์กด ํŒจํ‚ค์ง€ | `agents/`, `delegation/`, `atomic_yaml.py`, `layout.py`, `paths.py`, `state.py`, `verify_session.py`, `workspace_uuid.py` | +| `MAM_ENGINE_CAS` | **์•„์ง ์กด์žฌํ•˜์ง€ ์•Š์Œ** (`grep` ๊ฒฐ๊ณผ `implementation_plan.md` ์–ธ๊ธ‰ 1๊ฑด๋ฟ) โ€” Phase 2a๊ฐ€ ์‹ ์„ค | + +### 1-2. job ๋ ˆ์ฝ”๋“œ ์ƒํƒœ writer ์ „์ˆ˜ ์กฐ์‚ฌ (P2A-01์˜ ์‹ค์ œ ๋ฒ”์œ„) + +`_atomic_write_record` ์ง์ ‘ ํ˜ธ์ถœ์ž = **5๊ณณ**, ๊ทธ์ค‘ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฒƒ์€ **3๊ณณ**์ด๋‹ค. + +| # | ์œ„์น˜ | ํ•˜๋Š” ์ผ | CAS ์—ฌ๋ถ€ | +|---|---|---|---| +| W1 | `mqtt_common.py:410` (`update_job_status`) | `record.update(fields)` **๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ** | โŒ ์—†์Œ | +| W2 | `registry.py:131` (`pick_pending`) | ๋ฝ ์•ˆ์—์„œ `status == "pending"` ํ•„ํ„ฐ ํ›„ `running`์œผ๋กœ ๊ธฐ๋ก | โš ๏ธ **์‚ฌ์‹ค์ƒ CAS์ด๋‚˜ W1์„ ์šฐํšŒํ•œ ๋ณ„๋„ ๊ตฌํ˜„** | +| W3 | `mqtt_common.py:433` (`next_seq`) | `last_seq` ์ฆ๊ฐ€ (์ƒํƒœ ๋ฌด๊ด€) | ํ•ด๋‹น ์—†์Œ | +| โ€” | `registry.py:107` (`register_job`) | ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ | ํ•ด๋‹น ์—†์Œ | +| โ€” | `registry.py:151` (`update_status`) โ†’ W1 | | | +| โ€” | `registry.py:365` (`update` CLI) โ†’ W1 | | | +| โ€” | `publish_event.py:179` (`record_outcome`) โ†’ W1 | | | + +> **ํ•ต์‹ฌ (์ •ํ™•ํžˆ)**: `update_job_status`์˜ docstring์€ ์ž์‹ ์„ "the single chokepoint for status writes"๋ผ๊ณ  ์„ ์–ธํ•˜์ง€๋งŒ, `registry.py:135`์˜ ์ฃผ์„์ด ์Šค์Šค๋กœ ์ธ์ •ํ•˜๋“ฏ `pick_pending`์€ ์ด๋ฅผ ์šฐํšŒํ•œ๋‹ค. +> +> ๋‹จ, **`pick_pending`์ด ํ‹€๋ฆฐ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.** ๋ฝ ์•ˆ์—์„œ `status == "pending"`์„ ํ•„ํ„ฐํ•œ ๋’ค ๊ธฐ๋กํ•˜๋ฏ€๋กœ `pending โ†’ running`์— ๋Œ€ํ•ด์„œ๋Š” **์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅธ compare-and-swap**์ด๋‹ค(`registry.py:123`). ๋ฌธ์ œ๋Š” ์ •ํ™•์„ฑ์ด ์•„๋‹ˆ๋ผ **์ค‘๋ณต**์ด๋‹ค: CAS ํŒ์ •๊ณผ ๊ฐ์‚ฌ ๋กœ๊ทธ mirror๊ฐ€ ๋‘ ๋ฒŒ๋กœ ์กด์žฌํ•˜๋ฉฐ, Phase 1์˜ O-1(1ํšŒ์„ฑ ์ „์†ก ์ด์ค‘ ๊ตฌํ˜„)์—์„œ ํ™•์ธํ–ˆ๋“ฏ **๊ฐ™์€ ๋กœ์ง์˜ ์‚ฌ๋ณธ์€ ํ•œ์ชฝ์—๋งŒ ๊ฒฐํ•จ์ด ์ƒ๊ธฐ๋Š” ๊ฒฝ๋กœ**๋‹ค. +> +> ๋”ฐ๋ผ์„œ **P2A-01์€ "์—†๋Š” CAS๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“œ๋Š”" ์ž‘์—…์ด ์•„๋‹ˆ๋ผ "ํ•œ์ชฝ์—๋งŒ ์žˆ๋Š” CAS๋ฅผ ๋ชจ๋“  ์ „์ด๋กœ ์ผ๋ฐ˜ํ™”ํ•˜๊ณ  ๋‘ ์‚ฌ๋ณธ์„ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š”" ์ž‘์—…์ด๋‹ค.** ์‹ค์ œ๋กœ CAS๊ฐ€ ์ „ํ˜€ ์—†๋Š” ๊ฒƒ์€ W1(`record.update(fields)` ๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ)์ด๋‹ค. + +์…ธ/๋ž˜ํผ ์ธก์€ job ๋ ˆ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์“ฐ์ง€ ์•Š๋Š”๋‹ค: ๋ž˜ํผ๋Š” `registry.py status --set`(โ†’W1)์„, `reconcile.sh:192`๋Š” `registry.load_job`(์ฝ๊ธฐ ์ „์šฉ)๋งŒ ์“ด๋‹ค. `run_loop.sh:254`๋Š” job JSON์„ **์ฝ๊ธฐ๋งŒ** ํ•œ๋‹ค(3์ดˆ ํด๋ง). ์ฆ‰ **๋ชจ๋“  ์“ฐ๊ธฐ๊ฐ€ ํŒŒ์ด์ฌ ์•ˆ์— ์žˆ๋‹ค** โ€” CAS ๊ฒฝ๊ณ„๋ฅผ ํŒŒ์ด์ฌ ํ•œ ๊ณณ์— ๋‘๋ฉด ์ „์ˆ˜ ์ปค๋ฒ„๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. + +### 1-3. ํ˜„์žฌ ๋ฝ ํŠธ๋žœ์žญ์…˜์˜ I/O ์‹ค์ธก + +`update_job_status`(`:405-418`)๋Š” `registry_lock` **๋ณด์œ  ์ค‘์—** ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค: +- `load_job` (์ฝ๊ธฐ I/O) +- `_atomic_write_record` (์“ฐ๊ธฐ + `fsync` + `os.replace`) +- `update_logged_status` (**๊ฐ์‚ฌ ๋กœ๊ทธ ๋””๋ ‰ํ„ฐ๋ฆฌ ์“ฐ๊ธฐ**) +- `append_event` (**๊ฐ์‚ฌ ๋กœ๊ทธ append**) + +๋„คํŠธ์›Œํฌ/์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋Š” ๋ฝ ์•ˆ์— ์—†๋‹ค(`publish_event.main`์˜ ์ˆœ์„œ: `next_seq`(๋ฝ) โ†’ ๋ฐœํ–‰(๋„คํŠธ์›Œํฌ, ๋ฝ ์—†์Œ) โ†’ `record_outcome`(๋ฝ)). **์ด ์„ฑ์งˆ์€ Phase 2a์—์„œ ๋ฐ˜๋“œ์‹œ ๋ณด์กดํ•ด์•ผ ํ•˜๋ฉฐ**, ์ถ”๊ฐ€๋กœ ๊ฐ์‚ฌ ๋กœ๊ทธ I/O๋ฅผ ๋ฝ ๋ฐ–์œผ๋กœ ๋นผ๋Š” ๊ฒƒ์ด P2A-01์˜ "๋ฝ ๋‚ด I/O ๋ฐฐ์ œ" ์š”๊ตฌ๋‹ค. + +### 1-4. 4์ค‘ ๊ท€์†์— ํ•„์š”ํ•œ ํ•„๋“œ ๋ถ€์žฌ (์„ค๊ณ„๋ฅผ ์ขŒ์šฐํ•˜๋Š” ์‚ฌ์‹ค) + +๋ผ์ด๋ธŒ job ๋ ˆ์ฝ”๋“œ์˜ ์‹ค์ œ ํ‚ค: +``` +agent, agent_session, auth_token, broker, created_at, expected_artifacts, +idle_timeout_sec, iteration, job_id, job_type, last_seq, max_iterations, +prompt, reviewer, reviewer_session, role, schema_version, status, +timeout_sec, topic_prefix, updated_at +``` +`state-transitions.md` ยง2๊ฐ€ ์š”๊ตฌํ•˜๋Š” 4์ค‘ ๊ท€์† ์ค‘ **`attempt_id`ยท`agent_session_uuid`ยท`pane_process_generation`์ด ์ „๋ถ€ ์—†๋‹ค**. `iteration`(๋ฃจํ”„ ํšŒ์ฐจ)์€ ์žˆ์œผ๋‚˜ attempt์™€ ์˜๋ฏธ๊ฐ€ ๋‹ค๋ฅด๋‹ค. + +๋™์‹œ์— Phase 1์€ "์Šคํ‚ค๋งˆ ๋ถˆ๋ณ€(`SCHEMA_VERSION = 1`, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ ˆ์ฝ”๋“œ ํ•„๋“œ ๋ถˆ๋ณ€)"์„ ๊ณ„์•ฝ์œผ๋กœ ๋ชป๋ฐ•์•˜๊ณ , P2A-03์€ **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์“ฐ๊ธฐ ์ž์ฒด๋ฅผ ๊ธˆ์ง€**ํ•œ๋‹ค. โ†’ **Phase 2a์—์„œ ๊ท€์† ์ฆ๊ฑฐ๋ฅผ ๋ ˆ์ฝ”๋“œ์— ๋„ฃ๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋„ฃ์–ด์„œ๋„ ์•ˆ ๋œ๋‹ค.** ยงD6์ด ์ด๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค. + +### 1-5. `herdr agent wait` ๋ผ์ด๋ธŒ ๊ณ„์•ฝ (watcher.py์˜ ์ž…๋ ฅ) + +``` +Usage: herdr agent wait [OPTIONS] + --until idle|working|blocked|done|unknown (๋ฐ˜๋ณต ๊ฐ€๋Šฅ) + --timeout Fail after this many milliseconds +Without --until, matches idle, done, or blocked. +Without --timeout, waits indefinitely. +``` +- **๋‹จ์œ„๋Š” ๋ฐ€๋ฆฌ์ดˆ**๋‹ค. Phase 0 fixture(`wait-contract.json`)๋„ `"type": "integer_ms"`๋กœ ์ •ํ™•ํžˆ ๊ธฐ๋ก๋˜์–ด ์žˆ๋‹ค. +- ์„ฑ๊ณต โ†’ `exit 0`, **stdout**์— `{"id":"cli:agent:wait","result":{"agent":{...}}}`. +- ํƒ€์ž„์•„์›ƒ/๋Œ€์ƒ ๋ถ€์žฌ โ†’ `exit 1`, **stderr**์— ์—๋Ÿฌ JSON(`error.code` = `timeout` / `agent_not_found`). stdout์€ ๋น„์–ด ์žˆ๋‹ค. +- `--until` ์ƒ๋žต ์‹œ ๊ธฐ๋ณธ ๋งค์นญ์€ `idle,done,blocked` โ€” **`working`์€ ๋งค์นญ๋˜์ง€ ์•Š๋Š”๋‹ค.** watcher๋Š” ํ•ญ์ƒ `--until`์„ ๋ช…์‹œํ•œ๋‹ค. +- **๋‹จ์ผ `wait`๋Š” 1๊ฐœ์˜ ์ตœ์ข… ์Šค๋ƒ…์ƒท๋งŒ ์ฃผ๋Š” ๋™๊ธฐ ์žฅ๋ฒฝ**์ด๋‹ค(`herdr-runtime.md` ยง2-1). ์—ฐ์† ๊ด€์ธก์€ ๋ฐ˜๋ณต ํ˜ธ์ถœ๋กœ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค. + +### 1-6. Phase 0์—์„œ ์ด๋ฏธ ๊ฒ€์ฆ๋œ ํŒ์ • ๋ฒกํ„ฐ + +`tests/fixtures/issue_5/watcher/`์˜ 7์ข…๊ณผ `expected_policy.decision`์€ 3์ธ ๊ต์ฐจ ๋ฆฌ๋ทฐ๋ฅผ ํ†ต๊ณผํ•œ ๊ณ„์•ฝ์ด๋‹ค. Phase 2a ์—”์ง„์˜ ๊ฒฐ์ • ํ•จ์ˆ˜๋Š” **์ด 7์ข…์„ ๊ทธ๋Œ€๋กœ ์ž…๋ ฅ๋ฐ›์•„ ๊ฐ™์€ ๊ฒฐ์ •์„ ๋‚ด์•ผ ํ•œ๋‹ค**. + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ ๊ฒฐ์ • | +|---|---| +| `initial_idle` | `hold` (์‹คํ–‰ ์•ต์ปค ์—†์Œ) | +| `tool_running` | `hold` (๋„๊ตฌ ์‹คํ–‰ ์ค‘) | +| `report_pause_15s` | `hold` (๋งค๋‹ˆํŽ˜์ŠคํŠธ ์—†์Œ โ€” ๋””๋ฐ”์šด์Šค๋งŒ์œผ๋กœ ์™„๋ฃŒ ๊ธˆ์ง€) | +| `finalized_candidate` | `candidate_approved` | +| `blocked_permission` | `hold_blocked` | +| `transport_disconnected` | `hold_reconnect` | +| `restarted_generation` | `reject_stale` | + +`dispatch_context` ํ•„๋“œ: `job_id`, `attempt_id`, `agent_session_uuid`, `pane_process_generation`, `dispatch_ack`, `execution_anchor_observed`. + +### 1-7. Phase 1์—์„œ ๋„˜์–ด์˜จ ์ธ๊ณ„ ์‚ฌํ•ญ (๋ณธ Phase๊ฐ€ ๊ฒŒ์ดํŠธ) + +1. **`preexec_fn` (3์ธ ๋ฆฌ๋ทฐ์–ด ๊ณตํ†ต ์ง€์ )**: `daemon.spawn`์€ `preexec_fn`์œผ๋กœ SIGHUP์„ ๋ฌด์‹œํ•˜๋Š”๋ฐ ์ด๋Š” **๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ”„๋กœ์„ธ์Šค์—์„œ ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค**. Shadow watcher๊ฐ€ ์Šค๋ ˆ๋“œ๋ฅผ ์“ฐ๋ฉด์„œ `DelegationService.ensure_subscriber_daemon`์„ ํ˜ธ์ถœํ•˜๋ฉด ์ด ๊ฒŒ์ดํŠธ์— ๊ฑธ๋ฆฐ๋‹ค(ยงD9). +2. **O-4 (Claude)**: freeze ํŠธ๋ฆฌ์˜ `_delegate_py_bin`์€ `.venv`๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด paho ์—†๋Š” ์‹œ์Šคํ…œ python์œผ๋กœ ๋–จ์–ด์ง„๋‹ค. Phase 2a๊ฐ€ freeze `lib.sh` ํ—ฌํผ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š” ํ•œ ๋ฌดํ•ดํ•˜๋‹ค(ยงD9์—์„œ ์žฌํ™•์ธ). +3. **์—ฐ๊ฒฐ ์ง€๋ฌธ ํ•˜๋“œ์ฝ”๋”ฉ ํ•„๋“œ ๋ชฉ๋ก**: `BrokerConfig` ํ•„๋“œ ์ถ”๊ฐ€ ์‹œ ์กฐ์šฉํžˆ ๋ˆ„๋ฝ๋œ๋‹ค. Phase 2a์—์„œ `dataclasses.fields()` ์œ ๋„๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ์ปค๋ฒ„๋ฆฌ์ง€ ๋‹จ์ • ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค(ยง3 P2A-04์— ํฌํ•จ). + +--- + +## 2. ์„ค๊ณ„ ๊ฒฐ์ • (Design Decisions) + +### D1. ํŒจํ‚ค์ง€ ๋ฐฐ์น˜ + +``` +.agents/skills/lib_py/loop/ + __init__.py # ๊ณต๊ฐœ API ์žฌ์ˆ˜์ถœ + evidence.py # ์ฆ๊ฑฐ ํŠœํ”Œ ์ •๊ทœํ™” (watcher โ†’ engine ์‚ฌ์ด์˜ ์œ ์ผํ•œ ์ž๋ฃŒํ˜•) + attribution.py # 4์ค‘ ๊ท€์† + dispatch anchor ํŒ์ • (์ˆœ์ˆ˜ ํ•จ์ˆ˜) + policy.py # ๊ฒฐ์ • ํ•จ์ˆ˜: ์ฆ๊ฑฐ -> hold|hold_blocked|hold_reconnect|reject_stale|candidate_approved + engine.py # ๋‹จ์ผ ์ „์ด ์ฃผ์ฒด + CAS ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ + watcher.py # herdr agent wait ๋ž˜ํ•‘, ์žฌ์—ฐ๊ฒฐ ๋ฐฑ์˜คํ”„, ์Šค๋ƒ…์ƒท ์ˆ˜์ง‘ + shadow.py # Shadow ๋Ÿฌ๋„ˆ: watcher -> policy -> "would-complete" ๋กœ๊น… (์“ฐ๊ธฐ ์—†์Œ) + store.py # shadow ์ „์šฉ ๊ท€์†/์ฆ๊ฑฐ ์ €์žฅ์†Œ (๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ๋ถ„๋ฆฌ) +``` + +- **`policy.py`๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์—ฌ์•ผ ํ•œ๋‹ค**(I/Oยท์‹œ๊ณ„ยท์ „์—ญ ์ƒํƒœ ๊ธˆ์ง€). Phase 0 fixture 7์ข…์„ ๊ทธ๋Œ€๋กœ ๋จน์—ฌ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜ ์กฐ๊ฑด์ด๋‹ค. +- **D1(b) ์žฌ์ ์šฉ**: `lib_py.loop`๋Š” `lib_py.delegation` **์™ธ์˜** ํ˜•์ œ `lib_py` ๋ชจ๋“ˆ์„ ์ž„ํฌํŠธํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ด์œ ๋Š” Phase 1๊ณผ ๋™์ผํ•˜๋‹ค โ€” freeze ํ•˜์—์„œ ๋ถ€๋ชจ `lib_py`์™€ ๋ฐ”์ธ๋”ฉ๋œ ํŒจํ‚ค์ง€๊ฐ€ ๋‹ค๋ฅธ ํŠธ๋ฆฌ์ผ ์ˆ˜ ์žˆ๋‹ค. AST grep ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•œ๋‹ค(ยงD8). +- Python 3.9 ํ˜ธํ™˜ ํ•„์ˆ˜(`match`, PEP 604 ๋Ÿฐํƒ€์ž„ ์œ ๋‹ˆ์˜จ ๊ธˆ์ง€). ๊ฒŒ์ดํŠธ์—์„œ `compileall`. + +### D2. ๋‹จ์ผ CAS ์ „์ด ๊ฒฝ๊ณ„ (P2A-01) + +**`engine.transition()`์ด ์œ ์ผํ•œ ์ƒํƒœ writer๊ฐ€ ๋œ๋‹ค.** + +```python +# lib_py/loop/engine.py +class TransitionResult(object): + __slots__ = ("committed", "from_status", "to_status", "reason", "record", "notifications") + +def transition(job_id, request, *, registry_dir, expected_attempt=None, + expected_status=None, fields=None, source="unknown") -> TransitionResult: + """์œ ์ผํ•œ ์ƒํƒœ ์ „์ด ์ฃผ์ฒด. + + ๋ฝ ์•ˆ์—์„œ ํ•˜๋Š” ์ผ: load -> CAS ํŒ์ • -> (ํ—ˆ์šฉ ์‹œ) ๊ธฐ๋ก -> ์•Œ๋ฆผ ํ์ž‰. + ๋ฝ ๋ฐ–์—์„œ ํ•˜๋Š” ์ผ: ์•Œ๋ฆผ flush(๊ฐ์‚ฌ ๋กœ๊ทธ), MQTT ๋ฐœํ–‰. + """ +``` + +CAS ๊ทœ์น™์€ `state-transitions.md` ยง3 ํ‘œ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ตฌํ˜„ํ•œ๋‹ค: + +| ํ˜„์žฌ | ์š”์ฒญ | ์กฐ๊ฑด | ๊ฒฐ๊ณผ | +|---|---|---|---| +| `pending` | `claim`/`started` | ์„ธ์…˜ยทattempt ์ผ์น˜ | `running` | +| `running` | `progress`/`permission_required` | โ€” | `running` (๋ฉ”ํƒ€๋งŒ ๊ฐฑ์‹ ) | +| `running` | explicit terminal | ์œ ํšจ ์„ธ์…˜ ์„œ๋ช… | ์š”์ฒญ terminal | +| `terminal` | ๋™์ผ terminal ์žฌ์ˆ˜์‹  | โ€” | **No-op ๋ฉฑ๋“ฑ ACK** | +| `terminal` | ์ƒ์ถฉ ์‹ ํ˜ธ | โ€” | **๊ฑฐ๋ถ€ + conflict ๊ฐ์‚ฌ ๊ธฐ๋ก** | +| ์ž„์˜ | stale attempt/generation | ๋ถˆ์ผ์น˜ | **๊ฑฐ๋ถ€ + stale ๊ฒฝ๊ณ ** | +| `running` | `deadline_timeout` | ํƒ€์ž„์•„์›ƒ ํ™•์ธ | `error` | + +**๊ธฐ์กด writer 3๊ณณ์˜ ๋ผ์šฐํŒ…**: +- W1 `update_job_status` โ†’ ๋‚ด๋ถ€์ ์œผ๋กœ `engine.transition(request="merge", ...)`๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๊ต์ฒด. **์‹œ๊ทธ๋‹ˆ์ฒ˜ยท๋ฐ˜ํ™˜๊ฐ’ยท์˜ˆ์™ธ๋Š” ๋ถˆ๋ณ€**(Phase 1์˜ G-1~G-4๊ฐ€ ์ด ํ•จ์ˆ˜ ๊ณ„์•ฝ์— ๊ฑธ๋ ค ์žˆ๋‹ค). +- W2 `pick_pending` โ†’ `engine.transition(request="claim", expected_status="pending")`๋กœ ๊ต์ฒด. ์ด๋กœ์จ ์šฐํšŒ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค. +- W3 `next_seq` โ†’ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š์œผ๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋‘๋˜, ๊ฐ™์€ ๋ฝ์„ ์“ฐ๋ฏ€๋กœ **ํŠธ๋žœ์žญ์…˜ ์‹œ๊ฐ„ ์˜ˆ์‚ฐ**์—๋งŒ ํฌํ•จํ•œ๋‹ค. + +**๋กค๋ฐฑ ์Šค์œ„์น˜ `MAM_ENGINE_CAS`(๊ธฐ๋ณธ `1`)**: `0`์ด๋ฉด W1/W2๊ฐ€ **ํ˜„์žฌ์˜ ๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ ๋ณธ์ฒด**๋กœ ๋ถ„๊ธฐํ•œ๋‹ค. Phase 1์˜ `MAM_DELEGATION_PY`์™€ ๋™์ผํ•œ ํŒจํ„ด์ด๋ฉฐ, legacy ๋ณธ์ฒด๋Š” `_*_legacy`๋กœ ๋ณด์กดํ•œ๋‹ค(์ œ๊ฑฐ๋Š” Phase 4). + +### D3. ๋ฝ ๊ทœ์œจ โ€” ๋ฝ ์•ˆ์—์„œ๋Š” ๊ฒฐ์ •๋งŒ, I/O๋Š” ๋ฐ–์—์„œ + +``` +with registry_lock(registry_dir): # ์งง๊ฒŒ + record = load_job(...) + decision = _cas_decide(record, request, expected_attempt, expected_status) + if decision.commit: + _atomic_write_record(...) # ๋ ˆ์ฝ”๋“œ 1ํšŒ ์“ฐ๊ธฐ๋งŒ ํ—ˆ์šฉ + pending = decision.notifications # ๊ฐ์‚ฌ ๋กœ๊ทธ๋Š” "์“ฐ์ง€ ์•Š๊ณ " ํ์— ๋‹ด๋Š”๋‹ค +# ๋ฝ ํ•ด์ œ ํ›„ +_flush_notifications(pending) # ๊ฐ์‚ฌ ๋กœ๊ทธ append / status mirror +``` + +- **๊ธˆ์ง€**: ๋ฝ ๋ณด์œ  ์ค‘ `subprocess`, ์†Œ์ผ“, MQTT, `herdr` ํ˜ธ์ถœ, `time.sleep`. +- **ํ—ˆ์šฉ**: `load_job` 1ํšŒ + `_atomic_write_record` 1ํšŒ. +- ๊ฐ์‚ฌ ๋กœ๊ทธ mirror(`update_logged_status`/`append_event`)๋ฅผ ๋ฝ ๋ฐ–์œผ๋กœ ์˜ฎ๊ธด๋‹ค. **์ˆœ์„œ ๋ณด์žฅ ์†์‹ค ์šฐ๋ ค**: ํ˜„์žฌ๋Š” ๋ฝ ์•ˆ์ด๋ผ ๊ฐ์‚ฌ ์ด๋ ฅ์ด ์ง๋ ฌํ™”๋œ๋‹ค. ์ด๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์•Œ๋ฆผ์— **CAS๊ฐ€ ๋ถ€์—ฌํ•œ ๋‹จ์กฐ `transition_seq`**๋ฅผ ์‹ค์–ด ๋ณด๋‚ด๊ณ , flush๊ฐ€ ๋Šฆ๊ฒŒ ๋„์ฐฉํ•ด๋„ ์ˆœ์„œ๋ฅผ ๋ณต์›ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. +- ์ด ๊ทœ์œจ์€ ๋ฌธ์žฅ์ด ์•„๋‹ˆ๋ผ ํ…Œ์ŠคํŠธ๋กœ ๊ฐ•์ œํ•œ๋‹ค: ๋ฝ ํš๋“ ์ค‘ `subprocess.run`/`socket.socket`/`time.sleep`์„ monkeypatch๋กœ ๊ฐ์‹œํ•ด ํ˜ธ์ถœ 0๊ฑด์„ ๋‹จ์ •ํ•œ๋‹ค(G5). +- **์ €์žฅ์†Œ์— ์ด๋ฏธ ์„ ๋ก€๊ฐ€ ์žˆ๋‹ค**: `pick_pending`์€ ๋ ˆ์ฝ”๋“œ ์“ฐ๊ธฐ๋ฅผ ๋ฝ ์•ˆ์—์„œ ๋๋‚ด๊ณ  ๊ฐ์‚ฌ ๋กœ๊ทธ mirror(`update_logged_status`/`append_event`)๋ฅผ **๋ฝ ํ•ด์ œ ํ›„์—** ์ˆ˜ํ–‰ํ•œ๋‹ค(`registry.py:131-145`). D3์€ ์ƒˆ ํŒจํ„ด์„ ๋ฐœ๋ช…ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ **์ด ๊ธฐ์กด ํŒจํ„ด์„ `update_job_status`์—๋„ ์ ์šฉํ•ด ์ผ๋ฐ˜ํ™”**ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. + +### D4. `watcher.py` โ€” `herdr agent wait` ๋ž˜ํ•‘ + +```python +def wait_once(target, until=("idle", "done", "blocked"), timeout_ms=15000, run=subprocess.run) -> WaitOutcome +``` + +- `--timeout`์€ **๋ฐ€๋ฆฌ์ดˆ**๋กœ ์ „๋‹ฌํ•œ๋‹ค(ยง1-5). ์ดˆ ๋‹จ์œ„ ์ธ์ž๋ฅผ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹จ์œ„ ํ˜ผ๋™์„ ํƒ€์ž… ์ด๋ฆ„(`timeout_ms`)์œผ๋กœ ์ฐจ๋‹จํ•œ๋‹ค. +- `--until`์„ **ํ•ญ์ƒ ๋ช…์‹œ**ํ•œ๋‹ค(์ƒ๋žต ์‹œ `working`์ด ๋น ์ง€๋Š” ๊ธฐ๋ณธ๊ฐ’์— ์˜์กดํ•˜์ง€ ์•Š๋Š”๋‹ค). +- `exit 0` โ†’ stdout JSON ํŒŒ์‹ฑ โ†’ `result.agent` ์Šค๋ƒ…์ƒท. +- `exit 1` โ†’ **stderr** JSON ํŒŒ์‹ฑ โ†’ `error.code`๋กœ ๋ถ„๊ธฐ: `timeout`(์ •์ƒ์  ๋ฌด์ด๋ฒคํŠธ), `agent_not_found`(๋Œ€์ƒ ์†Œ๋ฉธ). +- ๊ทธ ์™ธ exit code / JSON ํŒŒ์‹ฑ ์‹คํŒจ / ์‹คํ–‰ ์‹คํŒจ โ†’ `transport_error`. +- **์žฌ์—ฐ๊ฒฐ ๋ฐฑ์˜คํ”„**: `transport_error` ์—ฐ์† ๋ฐœ์ƒ ์‹œ 1s โ†’ 2s โ†’ 4s โ†’ โ€ฆ โ†’ ์ตœ๋Œ€ 16s. ๋ฐฑ์˜คํ”„ ๋Œ€๊ธฐ๋Š” **๋ฝ ๋ฐ–์—์„œ๋งŒ** ํ•œ๋‹ค. ๋ณต๊ตฌ ์‹œ **์บ์‹œ๋œ ์Šค๋ƒ…์ƒท์„ ํ๊ธฐํ•˜๊ณ  ์žฌ์Šค๋ƒ…์ƒท**ํ•œ๋‹ค(`transport_disconnected` โ†’ `hold_reconnect` ์ •์ฑ…๊ณผ ์ผ์น˜). +- watcher๋Š” **ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค.** ์ฆ๊ฑฐ ํŠœํ”Œ๋งŒ ๋งŒ๋“ค์–ด ํ์— ๋„ฃ๋Š”๋‹ค(Phase 0 ํ•ฉ์˜: watcher๋Š” ๊ด€์ธก, engine์€ ๊ฒฐ์ •). + +### D5. Shadow ๋ชจ๋“œ โ€” ์“ฐ๊ธฐ ๋ถˆ๊ฐ€๋ฅผ ๊ตฌ์กฐ๋กœ ๋ณด์žฅ (P2A-03) + +```python +# shadow.py +class ShadowRunner: + def __init__(self, ..., decide=policy.decide, sink=logging_sink): + ... # engine ์„ ์•„์˜ˆ ์ฃผ์ž…๋ฐ›์ง€ ์•Š๋Š”๋‹ค +``` + +- **ShadowRunner๋Š” `engine.transition`์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค.** "์“ฐ์ง€ ์•Š๊ธฐ๋กœ ์•ฝ์†"์ด ์•„๋‹ˆ๋ผ **์“ธ ์ˆ˜๋‹จ์ด ์—†๋‹ค**. ์ด๊ฒƒ์ด P2A-03์„ ๋ฌธ์žฅ์ด ์•„๋‹ˆ๋ผ ๊ตฌ์กฐ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. +- ํŒ์ • ๊ฒฐ๊ณผ๋Š” `would_complete` ๋ ˆ์ฝ”๋“œ๋กœ **shadow ์ „์šฉ ๋กœ๊ทธ**์—๋งŒ ๋‚จ๋Š”๋‹ค: `.mam/shadow/.ndjson` (๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ๋‹ค๋ฅธ ๋””๋ ‰ํ„ฐ๋ฆฌ). +- ๊ฒ€์ฆ(G4): shadow ๋Ÿฌ๋„ˆ๋ฅผ ์‹œ๋‚˜๋ฆฌ์˜ค ์ „๋Ÿ‰์œผ๋กœ ๋Œ๋ฆฐ ์ „ํ›„์— **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ „์ฒด์˜ ํŒŒ์ผ ๋ชฉ๋กยทmtimeยทSHA-256์ด ๋ฐ”์ดํŠธ ๋™์ผ**ํ•จ์„ ๋‹จ์ •ํ•œ๋‹ค. "status๋ฅผ ์•ˆ ์ผ๋‹ค"๋ณด๋‹ค ๊ฐ•ํ•œ ๋‹จ์ •์ด๋‹ค. + +### D6. ๊ท€์† ์ฆ๊ฑฐ๋Š” shadow ์ „์šฉ ์ €์žฅ์†Œ์— (ยง1-4 ๊ฒฐ๋ก ) + +`store.py`๊ฐ€ `.mam/shadow/attribution/.json`์„ ์†Œ์œ ํ•œ๋‹ค. ๋‹ด๊ธฐ๋Š” ๊ฒƒ: + +```json +{ "job_id": "...", "attempt_id": 1, "agent_session_uuid": "...", + "pane_process_generation": "w2X:p1:1234:2026-09-06T00:00:00Z", + "dispatch_ack": true, "dispatch_ack_at": "...", "execution_anchor_observed": false } +``` + +- **job ๋ ˆ์ฝ”๋“œ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค** โ†’ Phase 1์˜ ์Šคํ‚ค๋งˆ ๋ถˆ๋ณ€ ๊ณ„์•ฝ๊ณผ P2A-03์„ ๋™์‹œ์— ๋งŒ์กฑํ•œ๋‹ค. +- `attempt_id`๋Š” Phase 2a์—์„œ **shadow๊ฐ€ ์ž์ฒด ๋ถ€์—ฌ**ํ•œ๋‹ค(๋””์ŠคํŒจ์น˜ ๊ด€์ธก ์‹œ 1๋ถ€ํ„ฐ). Phase 2b์—์„œ ๋ ˆ์ฝ”๋“œ๋กœ ์Šน๊ฒฉํ• ์ง€๋Š” ๊ทธ๋•Œ ๊ฒฐ์ •ํ•œ๋‹ค โ€” **์ด ๊ณ„ํš์„œ๋Š” ๋ ˆ์ฝ”๋“œ ์Šคํ‚ค๋งˆ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค.** +- `pane_process_generation`์€ `::` ํ˜•์‹์ด๋ฉฐ, Phase 0์˜ ์ •์ฒด์„ฑ ๊ฒ€์ฆ(`_validate_observed_herdr_item`)์ด ์ด๋ฏธ ์ด ํ˜•์‹์„ ๊ฐ•์ œํ•œ๋‹ค. + +### D7. ํŒ์ • ํ…Œ์ŠคํŠธ ๋ฒกํ„ฐ = Phase 0 fixture (P2A-04์˜ ์ ˆ๋ฐ˜) + +`policy.decide(scenario) -> decision` ์ด Phase 0์˜ 7์ข… ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•ด `expected_policy.decision`๊ณผ **์ •ํ™•ํžˆ ์ผ์น˜**ํ•ด์•ผ ํ•œ๋‹ค. ์‹ ๊ทœ fixture๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— **๊ธฐ์กด ๊ณ„์•ฝ์„ ๋จผ์ € ๋งŒ์กฑ**์‹œํ‚จ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด: +- ํŒ์ • ๋กœ์ง์ด 3์ธ ๋ฆฌ๋ทฐ๋ฅผ ํ†ต๊ณผํ•œ ๊ณ„์•ฝ์— ๋ฌถ์ธ๋‹ค. +- Phase 0์˜ `validate_watcher_scenario`๊ฐ€ fixture ๋ฌด๊ฒฐ์„ฑ์„ ๊ณ„์† ์ง€ํ‚จ๋‹ค. + +์ถ”๊ฐ€ ์‹œ๋‚˜๋ฆฌ์˜ค(P2A-04 ์š”๊ตฌ)๋Š” ๊ฐ™์€ ์Šคํ‚ค๋งˆ๋กœ ์ž‘์„ฑํ•˜๊ณ  **`validate_watcher_scenario`๋ฅผ ํ†ต๊ณผํ•ด์•ผ ํ•œ๋‹ค**: `race_double_dispatch`, `attempt_reuse`, `partial_report`, `unknown_status`, `restart_midflight`. + +### D8. ํŠธ๋ฆฌ ์ผ๊ด€์„ฑ (Phase 1 R1์˜ ๊ตํ›ˆ ์žฌ์ ์šฉ) + +- `lib_py.loop`๋Š” `lib_py.delegation`๋งŒ ํ˜•์ œ๋กœ ์ž„ํฌํŠธํ•œ๋‹ค. ๊ทธ ์™ธ `lib_py.*` ๊ธˆ์ง€ โ†’ AST ํ…Œ์ŠคํŠธ. +- ๋ฃจํ”„ ํ”„๋กœ์„ธ์Šค๋Š” shim์„ ์ธํ”„๋กœ์„ธ์Šค๋กœ ์ž„ํฌํŠธํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ `lib_py.loop`์™€ `lib_py.delegation`์€ ๋ถ€๋ชจ `lib_py`๋ฅผ ํ†ตํ•ด **๊ฐ™์€ ํŠธ๋ฆฌ**๋กœ ํ•ด์„๋œ๋‹ค. ์ด๋ฅผ ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•œ๋‹ค(`__file__` ์ ‘๋‘ ๋น„๊ต). +- Shadow ๋Ÿฌ๋„ˆ๋ฅผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋กœ ๋„์šธ ๋•Œ๋Š” Phase 1์˜ `_delegation_py`์™€ **๋™์ผํ•œ ๋ช…์‹œ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ**์„ ์“ด๋‹ค. `python -m`์€ cwd๊ฐ€ `PYTHONPATH`๋ฅผ ์ด๊ธฐ๋ฏ€๋กœ ๊ธˆ์ง€ํ•œ๋‹ค(R1 ์žฌ๋ฐœ ๋ฐฉ์ง€). + +### D9. Phase 1 ์ธ๊ณ„ ๊ฒŒ์ดํŠธ ํ•ด์†Œ + +- **`preexec_fn`**: Shadow watcher๊ฐ€ ์Šค๋ ˆ๋“œ๋ฅผ ์“ธ ๊ฒฝ์šฐ `daemon.spawn`์˜ `preexec_fn`์ด ์œ„ํ—˜ํ•ด์ง„๋‹ค. **Phase 2a์—์„œ๋Š” shadow ๋Ÿฌ๋„ˆ๊ฐ€ ๋ฐ๋ชฌ์„ ์Šคํฐํ•˜์ง€ ์•Š๋Š”๋‹ค**(๊ด€์ธก๋งŒ). `ensure_subscriber_daemon` ํ˜ธ์ถœ ๊ธˆ์ง€๋ฅผ grep ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•˜๊ณ , ์‹ค์ œ ์ œ๊ฑฐ/๋Œ€์ฒด๋Š” Phase 2b ์Šคํฐ ๋„์ž… ์‹œ ์ฒ˜๋ฆฌํ•œ๋‹ค. +- **O-4**: shadow๋Š” freeze `lib.sh` ํ—ฌํผ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ธํ„ฐํ”„๋ฆฌํ„ฐ ํ•ด์„ ๋ฌธ์ œ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค. ํ˜ธ์ถœ 0๊ฑด์„ grep์œผ๋กœ ๊ณ ์ •ํ•œ๋‹ค. +- **์—ฐ๊ฒฐ ์ง€๋ฌธ ํ•„๋“œ ๋ชฉ๋ก**: `dataclasses.fields(BrokerConfig)` ์ „๋Ÿ‰์ด ์ง€๋ฌธ์— ํฌํ•จ๋จ์„ ๋‹จ์ •ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค(ํ•„๋“œ ์ถ”๊ฐ€ ์‹œ ์กฐ์šฉํžˆ ๋ˆ„๋ฝ๋˜๋Š” ๊ฒƒ์„ ์ฐจ๋‹จ). + +--- + +## 3. ์ž‘์—… ๋ถ„ํ•ด (WBS) โ€” ์ปค๋ฐ‹ 5๊ฐœ + +### P2A-00 ํŠน์„ฑํ™” ์„ ํ–‰ (์ปค๋ฐ‹ 1: `test(issue-5): phase 2a writer inventory and lock discipline characterization`) +- **ํŒŒ์ผ**: `tests/test_issue5_phase2a_loop.py`(์‹ ๊ทœ) +- ํ˜„ํ–‰ ๋™์ž‘์„ **๋จผ์ € ๊ณ ์ •**ํ•œ๋‹ค(Phase 1์—์„œ ํšจ๊ณผ๊ฐ€ ์ž…์ฆ๋œ ์ˆœ์„œ): + - X-01 `update_job_status`๊ฐ€ ๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ์ž„์„ ๋‹จ์ •(ํ˜„ํ–‰ ์‚ฌ์‹ค). + - X-02 `pick_pending`์˜ ํ˜„ํ–‰ ๊ณ„์•ฝ ๊ณ ์ •: `pending`๋งŒ claimํ•˜๊ณ  ๋™์‹œ claim ์‹œ ํ•œ ์„ธ์…˜๋งŒ ์ด๊ธด๋‹ค(๋ผ์šฐํŒ… ํ›„์—๋„ ์ด ์„ฑ์งˆ์ด ์œ ์ง€๋˜์–ด์•ผ ํ•œ๋‹ค). + - X-03 ๋ฝ ๋ณด์œ  ์ค‘ ๊ฐ์‚ฌ ๋กœ๊ทธ I/O๊ฐ€ **๋ฐœ์ƒํ•จ**์„ ๋‹จ์ •(ํ˜„ํ–‰ ์‚ฌ์‹ค โ€” D3 ์ดํ›„ ์ด ํ…Œ์ŠคํŠธ๋Š” ๋ฐ˜๋Œ€๋กœ ๋’ค์ง‘ํžŒ๋‹ค). + - X-04 `next_seq`/`register_job`/`record_outcome` ๊ฒฝ๋กœ์˜ ํ˜„ํ–‰ ๋ถ€์ž‘์šฉ ์Šค๋ƒ…์ƒท. +- **์™„๋ฃŒ ๊ธฐ์ค€**: ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ์—์„œ ์ „๋ถ€ ํ†ต๊ณผ, 576 + ์‹ ๊ทœ ํ†ต๊ณผ. + +### P2A-01 ์—”์ง„ยทCAS ๊ฒฝ๊ณ„ (์ปค๋ฐ‹ 2: `feat(issue-5): single CAS transition boundary for every job-status writer`) +- **ํŒŒ์ผ**: `lib_py/loop/{__init__,engine,evidence,attribution}.py`; `mqtt_common.update_job_status`ยท`registry.pick_pending` ๋ผ์šฐํŒ…; `MAM_ENGINE_CAS` ๋ถ„๊ธฐ + `_*_legacy` ๋ณด์กด. +- **ํ…Œ์ŠคํŠธ**: `state-transitions.md` ยง3 ํ‘œ 9ํ–‰ ์ „๋ถ€์— ๋Œ€ํ•œ ์ „์ด ํ…Œ์ŠคํŠธ, ๋ฉฑ๋“ฑ ์žฌ์ˆ˜์‹ , ์ƒ์ถฉ ๊ฑฐ๋ถ€ + conflict ๊ฐ์‚ฌ, stale attempt ๊ฑฐ๋ถ€, `MAM_ENGINE_CAS=0` ์–‘ ๊ฒฝ๋กœ parametrize, **๋ฝ ๋‚ด I/O 0๊ฑด ๋‹จ์ •**(subprocess/socket/sleep monkeypatch). +- **์™„๋ฃŒ ๊ธฐ์ค€**: Phase 1์˜ G-1~G-4๊ฐ€ ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ(๊ณ„์•ฝ ๋ถˆ๋ณ€ ์ฆ๋ช…), ์ „์ฒด ์Šค์œ„ํŠธ ๋ฌดํšŒ๊ท€. + +### P2A-02 watcher (์ปค๋ฐ‹ 3: `feat(issue-5): herdr agent wait watcher with reconnect backoff`) +- **ํŒŒ์ผ**: `lib_py/loop/watcher.py`, `store.py` +- **ํ…Œ์ŠคํŠธ**: ๊ฐ€์งœ `run`์œผ๋กœ exit 0/stdout, exit 1/stderr `timeout`, exit 1/stderr `agent_not_found`, ํŒŒ์‹ฑ ์‹คํŒจ, ์‹คํ–‰ ์‹คํŒจ 5๊ฒฝ๋กœ; ๋ฐฑ์˜คํ”„ ์ˆ˜์—ด(1,2,4,8,16 ์ƒํ•œ)๊ณผ **์žฌ์—ฐ๊ฒฐ ์‹œ ์บ์‹œ ํ๊ธฐ**; `--timeout`์ด **๋ฐ€๋ฆฌ์ดˆ๋กœ ์ „๋‹ฌ**๋จ์„ argv ๋‹จ์ •; `--until` ํ•ญ์ƒ ๋ช…์‹œ๋จ์„ argv ๋‹จ์ •. **herdr ์‹ค์ œ ํ˜ธ์ถœ 0๊ฑด**(์ „๋ถ€ ์ฃผ์ž…๋œ ๊ฐ€์งœ). +- **์™„๋ฃŒ ๊ธฐ์ค€**: ์œ„ ํ†ต๊ณผ + ์‹ค์ œ herdr๋กœ 1ํšŒ ์Šค๋ชจํฌ(์ˆ˜๋™, ๋ณด๊ณ ์„œ ์ฒจ๋ถ€). + +### P2A-03 shadow ๋Ÿฌ๋„ˆ (์ปค๋ฐ‹ 4: `feat(issue-5): shadow watcher that logs would-complete without writing`) +- **ํŒŒ์ผ**: `lib_py/loop/{policy,shadow}.py` +- **ํ…Œ์ŠคํŠธ**: Phase 0 ์‹œ๋‚˜๋ฆฌ์˜ค 7์ข… โ†’ `policy.decide`๊ฐ€ `expected_policy.decision`๊ณผ ์ผ์น˜; ์‹ ๊ทœ ์‹œ๋‚˜๋ฆฌ์˜ค 5์ข… ์ถ”๊ฐ€(`validate_watcher_scenario` ํ†ต๊ณผ ํ•„์ˆ˜); **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋””๋ ‰ํ„ฐ๋ฆฌ SHA-256 ์ „ํ›„ ๋™์ผ** ๋‹จ์ •; `ShadowRunner`๊ฐ€ `engine`/`update_job_status`/`_atomic_write_record`๋ฅผ ์ฐธ์กฐํ•˜์ง€ ์•Š์Œ์„ AST๋กœ ๋‹จ์ •. +- **์™„๋ฃŒ ๊ธฐ์ค€**: shadow ์ „๋Ÿ‰ ์‹คํ–‰ ํ›„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฐ”์ดํŠธ ๋ถˆ๋ณ€. + +### P2A-04 ํ†ตํ•ฉยท๋ฌธ์„œ (์ปค๋ฐ‹ 5: `docs(issue-5): phase 2a contracts, scenarios and task checklist`) +- race/attempt ์žฌ์‚ฌ์šฉ/๋ถ€๋ถ„ ๋ฆฌํฌํŠธ/๋„๊ตฌ ์‹คํ–‰/blocked/unknown/์žฌ์‹œ์ž‘ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹คํ–‰ ๊ฒฐ๊ณผ ๊ธฐ๋ก. +- `state-transitions.md`์— ๊ตฌํ˜„ ๋งคํ•‘ ๊ฐ์ฃผ, `task.md` P2A-01~04 ์ฒดํฌ + ์ฆ๊ฑฐ, `implementation_plan.md` Phase 2a ์ƒํƒœ ๊ฐฑ์‹ , `MAM_ENGINE_CAS` ์šด์˜ ๋ฌธ์„œ. +- Phase 1 ์ธ๊ณ„ 3๊ฑด ์ฒ˜๋ฆฌ ์ƒํƒœ ๋ช…์‹œ(ยงD9). + +--- + +## 4. ๊ฒŒ์ดํŠธ + +| # | ์กฐ๊ฑด | ์ธก์ • | +|---|---|---| +| G1 | ๊ธฐ์กด **576** ์ „๋ถ€ ํ†ต๊ณผ + ์‹ ๊ทœ ์ „๋ถ€ ํ†ต๊ณผ, skipped 0 | `pytest tests/ -q` (์•ฝ 13๋ถ„) | +| G2 | ์…ธ ๋ฌธ๋ฒ•ยท๊ณต๋ฐฑ | `bash -n`, `git diff --check` (**๋ฒ”์œ„** ๊ธฐ์ค€) | +| G3 | Python 3.9 ์ปดํŒŒ์ผ | `compileall .agents/skills/lib_py/loop` | +| **G4** | **Shadow๊ฐ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— 0๋ฐ”์ดํŠธ ๊ธฐ๋ก** | ์‹คํ–‰ ์ „ํ›„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋””๋ ‰ํ„ฐ๋ฆฌ ํŒŒ์ผ ๋ชฉ๋กยทSHA-256 ๋™์ผ | +| **G5** | **๋ฝ ๋‚ด I/O ๋ฐฐ์ œ** | ๋ฝ ๋ณด์œ  ์ค‘ subprocess/socket/sleep ํ˜ธ์ถœ 0๊ฑด (monkeypatch ๊ฐ์‹œ) | +| G6 | ๋ชจ๋“  ์ƒํƒœ writer๊ฐ€ CAS ๊ฒฝ์œ  | `_atomic_write_record` ์ง์ ‘ ํ˜ธ์ถœ์ž๊ฐ€ engineยท`register_job`ยท`next_seq`๋ฟ์ž„์„ AST๋กœ ๋‹จ์ • | +| G7 | ํŒ์ •์ด Phase 0 ๊ณ„์•ฝ๊ณผ ์ผ์น˜ | ์‹œ๋‚˜๋ฆฌ์˜ค 7์ข… + ์‹ ๊ทœ 5์ข… `expected_policy` ์ผ์น˜ | +| G8 | ๋กค๋ฐฑ | `MAM_ENGINE_CAS=0`์—์„œ ํŠน์„ฑํ™” ํ†ต๊ณผ + ๋ผ์ด๋ธŒ ์Šค๋ชจํฌ | +| G9 | ํŠธ๋ฆฌ ์ผ๊ด€์„ฑ | `lib_py.loop`/`lib_py.delegation` ๋™์ผ ํŠธ๋ฆฌ, ํ˜•์ œ ์ž„ํฌํŠธ 0๊ฑด | + +Worker ๋ณด๊ณ ์„œ์—๋Š” G1 pytest ์š”์•ฝ ์›๋ฌธ, G4์˜ ํ•ด์‹œ ๋น„๊ต ์ถœ๋ ฅ, G5์˜ ๊ฐ์‹œ ๋กœ๊ทธ, G7 ํ‘œ๋ฅผ **๊ทธ๋Œ€๋กœ** ๋ถ™์ธ๋‹ค. `file:line` ์ธ์šฉ์€ `grep -n` ์ถœ๋ ฅ์œผ๋กœ ๋’ท๋ฐ›์นจํ•œ๋‹ค. + +--- + +## 5. ๋กค๋ฐฑ + +1. **์ฆ‰์‹œ**: `MAM_ENGINE_CAS=0` โ†’ W1/W2๊ฐ€ ๋ณด์กด๋œ legacy ๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ์œผ๋กœ ๋ถ„๊ธฐ. Shadow๋Š” ์• ์ดˆ์— ์“ฐ๊ธฐ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ๋งŒ์œผ๋กœ ํ”์ ์ด ์‚ฌ๋ผ์ง„๋‹ค. +2. **ํ”„๋กœ์„ธ์Šค**: Shadow ๋Ÿฌ๋„ˆ ์ข…๋ฃŒ + ์ธ๋ฉ”๋ชจ๋ฆฌ ํ ํ๊ธฐ. `.mam/shadow/`๋Š” ์‚ญ์ œํ•ด๋„ ๋ฌดํ•ด(๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Œ). +3. **์ปค๋ฐ‹ ๋‹จ์œ„**: 5โ†’4โ†’3โ†’2 ์ˆœ `git revert`. ์ปค๋ฐ‹ 1(ํŠน์„ฑํ™”)์€ ๋ ˆ๊ฑฐ์‹œ์—์„œ๋„ ํ†ต๊ณผํ•˜๋ฏ€๋กœ ๋‚จ๊ฒจ๋„ ๋œ๋‹ค. + +--- + +## 6. ๋ฆฌ์Šคํฌ์™€ ์™„ํ™” + +| # | ๋ฆฌ์Šคํฌ | ์™„ํ™” | +|---|---|---| +| R1 | CAS ๋„์ž…์ด Phase 1์˜ G-1~G-4 ๊ณ„์•ฝ์„ ๊นฌ๋‹ค | `update_job_status` ์‹œ๊ทธ๋‹ˆ์ฒ˜ยท๋ฐ˜ํ™˜ยท์˜ˆ์™ธ ๋ถˆ๋ณ€ ์œ ์ง€, ๋‘ ๊ฒฝ๋กœ parametrize | +| R2 | ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๋ฝ ๋ฐ–์œผ๋กœ ๋นผ๋ฉด์„œ ์ˆœ์„œ๊ฐ€ ๋’ค๋ฐ”๋€๋‹ค | CAS๊ฐ€ ๋ถ€์—ฌํ•˜๋Š” ๋‹จ์กฐ `transition_seq`๋ฅผ ์•Œ๋ฆผ์— ์‹ค์–ด ์ˆœ์„œ ๋ณต์› | +| R3 | `pick_pending` ๋ผ์šฐํŒ…์ด **์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅธ** ๋™์‹œ claim ๊ฒฝ์Ÿ์„ ๊นจ๋œจ๋ฆฐ๋‹ค | ๋ผ์šฐํŒ… ์ „์— X-02๋กœ ํ˜„ํ–‰ ์„ฑ์งˆ์„ ๊ณ ์ •ํ•˜๊ณ , `expected_status="pending"` CAS๊ฐ€ ๋™์ผ ์„ฑ์งˆ์„ ์œ ์ง€ํ•จ์„ ๋™์‹œ claim ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆ | +| R4 | `--timeout` ๋‹จ์œ„ ํ˜ผ๋™(์ดˆ vs ms) | ์ธ์ž๋ช… `timeout_ms`, argv ๋‹จ์ • ํ…Œ์ŠคํŠธ | +| R5 | shadow๊ฐ€ ์‹ค์ˆ˜๋กœ ์“ฐ๊ธฐ ๊ฒฝ๋กœ๋ฅผ ์–ป๋Š”๋‹ค | ๊ตฌ์กฐ์  ์ฐจ๋‹จ(engine ๋ฏธ์ฃผ์ž…) + AST ๋‹จ์ • + ํ•ด์‹œ ๋ถˆ๋ณ€ ๋‹จ์ • | +| R6 | freeze/live ํŠธ๋ฆฌ ํ˜ผ์žฌ | D8 (ํ˜•์ œ ์ž„ํฌํŠธ ๊ธˆ์ง€ + ๋ช…์‹œ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ) | +| R7 | `preexec_fn` ์Šค๋ ˆ๋“œ ๋น„์•ˆ์ „ | Phase 2a๋Š” ๋ฐ๋ชฌ ์Šคํฐ ์—†์Œ(grep ๊ณ ์ •), ์‹ค์ œ ๋Œ€์ฒด๋Š” Phase 2b | +| R8 | ์‹ค๋ธŒ๋กœ์ปค/์‹คherdr ์˜์กด ํ…Œ์ŠคํŠธ | ์ „๋ถ€ ์ฃผ์ž…๋œ ๊ฐ€์งœ, ์‹คํ˜ธ์ถœ 0๊ฑด. ๋ผ์ด๋ธŒ ์Šค๋ชจํฌ๋Š” ์ˆ˜๋™ยท์ •๋ณด์„ฑ | + +--- + +## 7. ๋ช…์‹œ์  ๋น„๋ฒ”์œ„ + +- **์ž๋™์™„๋ฃŒ ์‹ค์ œ ์ปค๋ฐ‹** โ€” Phase 2b. Phase 2a๋Š” `would_complete` ๋กœ๊น…๊นŒ์ง€๋งŒ. +- job ๋ ˆ์ฝ”๋“œ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ(`attempt_id` ๋“ฑ ์Šน๊ฒฉ) โ€” Phase 2b. +- `run_loop.sh`์˜ ํŒŒ์ด์ฌํ™” โ€” Phase 3. +- ๋ž˜ํผยท`reconcile.sh`์˜ ์ƒํƒœ ๋จธ์‹  ๋ณ€๊ฒฝ โ€” ์ „ Phase ๊ณตํ†ต ๋ถˆ๋ณ€. +- `preexec_fn` ์ œ๊ฑฐ โ€” Phase 2b. +- `pgrep` ๋‹ค์ค‘ PID ์ •๋ฆฌ, Windows โ€” Phase 4 ํ›„๋ณด. + +--- + +## 8. Worker ์ธ์ˆ˜์ธ๊ณ„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] P2A-00 ์ปค๋ฐ‹ 1: ํ˜„ํ–‰ ๋™์ž‘ ํŠน์„ฑํ™”(W1 ๋ฌด์กฐ๊ฑด ๋ณ‘ํ•ฉ, W2 ์šฐํšŒ, ๋ฝ ๋‚ด I/O ๋ฐœ์ƒ)๊ฐ€ **๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ์—์„œ** ํ†ต๊ณผ +- [ ] P2A-01 ์ปค๋ฐ‹ 2: `engine.transition` ๋‹จ์ผ ๊ฒฝ๊ณ„, W1/W2 ๋ผ์šฐํŒ…, `MAM_ENGINE_CAS` ์–‘ ๊ฒฝ๋กœ, ๋ฝ ๋‚ด I/O 0๊ฑด +- [ ] P2A-02 ์ปค๋ฐ‹ 3: watcher 5๊ฒฝ๋กœ + ๋ฐฑ์˜คํ”„ + ms ๋‹จ์œ„ argv ๋‹จ์ • +- [ ] P2A-03 ์ปค๋ฐ‹ 4: Phase 0 ์‹œ๋‚˜๋ฆฌ์˜ค 7์ข… ์ผ์น˜ + ์‹ ๊ทœ 5์ข…, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํ•ด์‹œ ๋ถˆ๋ณ€ +- [ ] P2A-04 ์ปค๋ฐ‹ 5: ๋ฌธ์„œยท์ฒดํฌ๋ฆฌ์ŠคํŠธยท์ธ๊ณ„ 3๊ฑด ์ฒ˜๋ฆฌ ์ƒํƒœ +- [ ] ๋ณด๊ณ ์„œ: G1 ์›๋ฌธ, G4 ํ•ด์‹œ ์ถœ๋ ฅ, G5 ๊ฐ์‹œ ๋กœ๊ทธ, G7 ํ‘œ, `grep -n` ์ธ์šฉ +- [ ] ์ปค๋ฐ‹ ์ ‘๋‘: `test(issue-5)`, `feat(issue-5)`, `feat(issue-5)`, `feat(issue-5)`, `docs(issue-5)` + +--- + +## ๋ถ€๋ก A. ๊ณ„ํš ์ˆ˜๋ฆฝ ์‹œ ์‹คํ–‰ํ•œ ๊ฒ€์ฆ ๋ช…๋ น (์žฌํ˜„์šฉ) + +```bash +git log --oneline -1 # 015ab70 +.venv/bin/python -m pytest tests/ --collect-only -q | tail -1 # 576 +grep -rn "_atomic_write_record" --include='*.py' .agents | grep -v "def " # writer 5๊ณณ +sed -n '125,155p' .agents/skills/lib_py/delegation/registry.py # pick_pending ์šฐํšŒ +sed -n '395,420p' .agents/skills/lib_py/delegation/mqtt_common.py # ๋ฝ ๋‚ด ๊ฐ์‚ฌ I/O +HERDR_SESSION_NAME=multi-agent-mux herdr agent wait --help # --timeout +cat tests/fixtures/issue_5/herdr/wait-contract.json # integer_ms +grep -rn "MAM_ENGINE_CAS" --include='*.py' --include='*.sh' . # ๋ฏธ์กด์žฌ +.venv/bin/python -c "import json,glob; print(sorted(json.load(open(sorted(glob.glob('.mam/jobs/*.json'))[-1]))))" # ๋ ˆ์ฝ”๋“œ ํ‚ค (attempt_id ๋ถ€์žฌ) +```