# ๐Ÿ“‘ Issue #5 Baseline Contract Specification - **Document Version**: Rev.3 (Post Cross-Review Corrections) - **Issue**: [Gitea Issue #5 โ€” `[Refactor] lib.sh ์ž‘์—… ์œ„์ž„ ๋กœ์ง ๋ฐ run_loop.sh ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ์˜ Python ๋ชจ๋“ˆํ™” (lib_py ์Šน๊ฒฉ)`](https://git.godopu.com/tmpl/multi-agent-mux/issues/5) - **Baseline Commit**: `47640e2fd7b108be29446a9384a343727e87edf8` - **Environment Baseline**: Python 3.9.6, Herdr 0.8.2, macOS Darwin 24.3.0 - **Test Suite Baseline**: 469 tests collected initially; 510 tests collected with Phase 0 contract suite (+41 contract & mutation tests) - **Status**: Phase 0 Baseline Contract (Observed vs. Proposed vs. Gap) --- ## 1. ๊ฐœ์š” ๋ฐ ๊ณ„์•ฝ ์ •์˜ ์›์น™ ๋ณธ ๋ฌธ์„œ๋Š” Issue #5(Python ๋ชจ๋“ˆํ™” ๋ฐ `lib_py` ์Šน๊ฒฉ)์˜ ์ฐฉ์ˆ˜ ๊ธฐ์ค€์„ (Baseline)์œผ๋กœ์„œ, ํ˜„์žฌ ์ฝ”๋“œ๋ฒ ์ด์Šค(`run_loop.sh`, `lib.sh`, `delegate-job` ๋„๊ตฌ๊ตฐ)์— ๊ตฌํ˜„๋˜์–ด ๋™์ž‘ ์ค‘์ธ CLI, ์ด๋ฒคํŠธ, ๋ฆฌํฌํŠธ, ๋ฝ(Lock) ๋ฐ ์ƒํƒœ ์ „์ด ๊ณ„์•ฝ์„ ์—„๋ฐ€ํ•˜๊ฒŒ ๋ถ„๋ฅ˜ํ•˜์—ฌ ๋ช…๋ฌธํ™”ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ•ญ๋ชฉ์€ ๋‹ค์Œ ์„ธ ๋ฒ”์ฃผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค: - **`[Observed]`**: ํ˜„์žฌ ์†Œ์Šค ์ฝ”๋“œ ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์‹ค์ œ๋กœ ํ™•์ธ๋œ ๋™์ž‘. - **`[Proposed]`**: ํ–ฅํ›„ Phase 1โ€“4์—์„œ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•˜๋Š” ๋ชฉํ‘œ ๋™์ž‘. - **`[Gap]`**: ํ˜„ํ–‰ ๋™์ž‘๊ณผ ๋ชฉํ‘œ ๋™์ž‘ ์‚ฌ์ด์˜ ๋ถˆ์ผ์น˜, ๊ฒฐํ•จ ๋˜๋Š” ๋ฏธ๊ตฌํ˜„ ์‚ฌํ•ญ. --- ## 2. CLI ์ธํ„ฐํŽ˜์ด์Šค ๊ณ„์•ฝ (Orchestrator & Delegation) ### 2-1. `run_loop.sh` CLI ์˜ต์…˜ ๋ฐ ์ธ์ˆ˜ ํŒŒ์‹ฑ ๊ณ„์•ฝ | ์˜ต์…˜ | ๊ธฐ๋ณธ๊ฐ’ | ์ œ์•ฝ ๋ฐ ๊ฒ€์ฆ ๊ทœ์น™ | ํ˜„ํ–‰ ์†Œ์Šค ์œ„์น˜ | ๋Œ€์‘ ํ…Œ์ŠคํŠธ | ๋ฒ”์ฃผ | |---|---|---|---|---|---| | `--creator ` | ์—†์Œ (ํ•„์ˆ˜) | ๋น„์–ด์žˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด ํ•„์ˆ˜(L95). ์„ธ์…˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(`.mam/agent-sessions.yaml` / `.db`) ๋“ฑ๋ก ๋ฐ ์‹คํ–‰ ์ค‘ ๊ฒ€์ฆ์€ L160โ€“185์—์„œ ์ˆ˜ํ–‰ | `run_loop.sh:79, 95-98, 160-185` | `test_loop_cli.py:test_creator_parses_and_reaches_session_check` | `[Observed]` | | `--task ` | ์—†์Œ (ํ•„์ˆ˜) | ์ž‘์—… ๋ชฉํ‘œ ๋ฌธ์ž์—ด. ๋ˆ„๋ฝ ์‹œ fast-fail ๋ฐ `usage` ์ถœ๋ ฅ ํ›„ exit 1 | `run_loop.sh:85, 95-98` | `test_loop_cli.py:test_missing_creator_and_task_fail_fast` | `[Observed]` | | `--target-agent ` | ์ œ๊ฑฐ๋จ | ์‚ฌ์šฉ ์ฆ‰์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ํ›„ exit 1 ๋ฐ˜ํ™˜ (ํ•˜์œ„ ํ˜ธํ™˜ ํ๊ธฐ) | `run_loop.sh:80-83` | `test_loop_cli.py:test_target_agent_is_rejected` | `[Observed]` | | `--plan` | `false` | ํ”Œ๋ž˜๋„ˆ ๊ฐœ์ž… ๋ฐ ์„ค๊ณ„ ๋‹จ๊ณ„ ํ™œ์„ฑํ™” ํ”Œ๋ž˜๊ทธ | `run_loop.sh:56` | `test_loop_cli.py:test_planner_without_plan_fail_fast` | `[Observed]` | | `--planner ` | ์ž๋™ ์กฐํšŒ | `--plan` ํ”Œ๋ž˜๊ทธ ์—†์ด ์ง€์ • ์‹œ ์ฆ‰์‹œ exit 1 ๋ฐ˜ํ™˜. ๋ฏธ์ง€์ • ์‹œ ๋“ฑ๋ก๋œ planner ์—ญํ•  ์„ธ์…˜ ์ž๋™ ์„ ํƒ | `run_loop.sh:84, 100-105` | `test_loop_cli.py:test_explicit_planner_unregistered`, `test_explicit_planner_skips_auto_resolve` | `[Observed]` | | `--plan-talk ` | `1` | ์ˆซ์ž regex(`^[0-9]+$`) ๊ฒ€์ฆ. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ์–‘์ˆ˜๋ฅผ ์š”๊ตฌํ•˜์ง€๋งŒ regex๋Š” **`0`์„ ํ—ˆ์šฉ**ํ•จ. 0 ์ง€์ • ์‹œ ํ† ๋ก  ๋ฃจํ”„(`$turn -le $PLAN_TALK_TURNS`)๋ฅผ ์™„์ „ํžˆ ๊ฑด๋„ˆ๋œ€ | `run_loop.sh:57-62, 442` | `test_loop_cli.py` (regex ํŒŒ์‹ฑ) | `[Observed]` | | `--reviewer ` | ์—†์Œ | ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฆฌ๋ทฐ์–ด ์„ธ์…˜ ๋ชฉ๋ก | `run_loop.sh:63` | `test_loop_cli.py` | `[Observed]` | | `--all-reviewer` | `false` | ๋ชจ๋“  ํ™œ์„ฑ ๋ฆฌ๋ทฐ์–ด ์„ธ์…˜ ๊ฐ•์ œ. `--reviewer`์™€ ๋™์‹œ ์ง€์ • ์‹œ **`--all-reviewer`๊ฐ€ ์šฐ์„ **ํ•˜๋ฉฐ `--reviewer` ๋ชฉ๋ก์€ ๋ฌด์‹œ๋˜๊ณ  ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ถœ๋ ฅ | `run_loop.sh:64, 192-196` | ํ˜„ํ–‰ ์ „์šฉ CLI ํ…Œ์ŠคํŠธ ๋ถ€์žฌ (`[Gap]`) | `[Observed]` | | `--max-loop ` | `3` | ์–‘์ˆ˜ ์ •์ˆ˜ ๊ฒ€์ฆ(`^[0-9]+$` and `N > 0`). 0 ์ดํ•˜ ์‹œ ์—๋Ÿฌ ๋ฐ˜ํ™˜ | `run_loop.sh:65-70` | `test_loop_cli.py` | `[Observed]` | | `--max-rebut ` | `1` | ๋น„์Œ์ˆ˜ ์ •์ˆ˜ ๊ฒ€์ฆ(`^[0-9]+$`). 0 ์ง€์ • ์‹œ ๋ฐ˜๋ก (Rebuttal) ์™„์ „ ๋น„ํ™œ์„ฑํ™” | `run_loop.sh:71-76` | `test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget` | `[Observed]` | | `REBUT_TOTAL_BUDGET` | `MAX_REBUT * MAX_LOOP` | ์ „์ฒด ์‹คํ–‰ ๊ธฐ๊ฐ„ ๋™์•ˆ์˜ ์ด ๋ฐ˜๋ก  ํ—ˆ์šฉ ์ƒํ•œ(Run-wide budget). ์กฐ๊ธฐ ์†Œ์ง„ ์‹œ ์ถ”๊ฐ€ ๋ฐ˜๋ก  ๊ธˆ์ง€ | `run_loop.sh:93, 762-766` | `test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget` | `[Observed]` | | `--verbose` | `false` | ์ƒ์„ธ ์‹คํ–‰ ํƒ€์ž„๋ผ์ธ ์ถ”์  ์ถœ๋ ฅ ํ™œ์„ฑํ™” ํ”Œ๋ž˜๊ทธ | `run_loop.sh:77` | N/A | `[Observed]` | | `--cleanup` | `false` | ๋ฃจํ”„ ์„ฑ๊ณต ์ข…๋ฃŒ ์‹œ ์ƒ์„ฑ๋œ ์ž„์‹œ ์ž‘์—… ๋””๋ ‰ํ„ฐ๋ฆฌ(`.mam/jobs/`) ๋ฐ ๊ฐ€์ž…์ž ์ถœ๋ ฅ ํŒŒ์ผ ์‚ญ์ œ | `run_loop.sh:78, 921-930` | N/A | `[Observed]` | | `-h`, `--help` | N/A | ๋„์›€๋ง ์ถœ๋ ฅ ํ›„ **exit 1**๋กœ ๋น„์ •์ƒ ์ข…๋ฃŒ (ํ˜„์žฌ ์ฝ”๋“œ์˜ ๊ด€ํ–‰์  ๋™์ž‘) | `run_loop.sh:50, 86` | `test_loop_cli.py:test_help_lists_creator_not_target_agent` | `[Observed]` | > โš ๏ธ **Help Exit Code Gap (`[Gap]`)**: > ํ˜„์žฌ `run_loop.sh:50`์€ `usage()` ๋‚ด๋ถ€์—์„œ `exit 1`์„ ํ˜ธ์ถœํ•˜์—ฌ `--help` ์š”์ฒญ ์‹œ์—๋„ exit 1์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ‘œ์ค€ CLI ๊ด€๋ก€(exit 0)์™€ ๋‹ค๋ฅด๋‚˜, ๊ธฐ์กด ์„œ๋ธŒํ”„๋กœ์„ธ์Šค ํ…Œ์ŠคํŠธ๋“ค์ด `exit 1`์— ์˜์กดํ•˜๋ฏ€๋กœ Phase 0์—์„œ๋Š” ํ˜„ํ–‰ ๋™์ž‘์„ ์œ ์ง€ํ•˜๋ฉฐ ์˜๋„์  ๋ณ€๊ฒฝ์€ Phase 3 wrapper ์Šน๊ฒฉ ์‹œ ๋ณ„๋„ Decision์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ### 2-2. ํ™˜๊ฒฝ, ๊ฒฝ๋กœ ๋ฐ ์ˆ˜๋ช…์ฃผ๊ธฐ ๊ณ„์•ฝ 1. **ํŒŒ์ด์ฌ ์ธํ„ฐํ”„๋ฆฌํ„ฐ ํƒ์ƒ‰ (`_delegate_py_bin`)**: - `AGENT_PYTHON_BIN` โž” `VIRTUAL_ENV/bin/python` โž” ์ƒ์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ `.venv/bin/python` ํƒ์ƒ‰ โž” ์‹œ์Šคํ…œ `python3` ์ˆœ์œผ๋กœ ์บ์‹ฑ ๋ฐ ๊ฒฐ์ • (`lib.sh:1841-1861`). `[Observed]` 2. **๋Ÿฐํƒ€์ž„ ๊ณ ์ • ๋ณต์‚ฌ๋ณธ (B-13 Freeze Snapshot)**: - ๋ฃจํ”„ ๊ธฐ๋™ ์‹œ `.agents/skills/` ํŠธ๋ฆฌ๋ฅผ `/tmp/mam-loop-freeze.XXXXXX`๋กœ ๋ณต์‚ฌ ํ›„ ํ•ด๋‹น ์œ„์น˜์˜ `run_loop.sh`๋กœ `exec` ์žฌ์‹คํ–‰ (`run_loop.sh:112-127`). `[Observed]` - ๋‹จ, `multi-agent-mux-create`, `resume`, `stop` ๋“ฑ 1ํšŒ์„ฑ ์Šคํฌ๋ฆฝํŠธ๋Š” Live Tree๋ฅผ ์ฐธ์กฐ (`[Observed]`). 3. **๋ฃจํ”„ ๊ฐ€๋“œ ๋ฐ ์‹œ๊ทธ๋„ ์ •๋ฆฌ**: - `MAM_LOOP_MARKER`(`.mam/loop-guard-active`)์— `pid`, `lstart`, `started` ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์›์ž์  ํ•˜๋“œ๋งํฌ๋กœ ๊ธฐ๋ก (`loop_lock.sh:69-75`). `[Observed]` - `trap _mam_release_guard EXIT INT TERM HUP`์„ ํ†ตํ•ด ๋น„์ •์ƒ ์ข…๋ฃŒ ์‹œ์—๋„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€๋“œ ํŒŒ์ผ ๋ฐ Freeze ์ž„์‹œ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ •๋ฆฌ (`run_loop.sh:189`). `[Observed]` 4. **์ž‘์—… ์œ„์ž„ ํ•จ์ˆ˜ ๊ณ„์•ฝ**: - `delegate_submit_job`: `registry.py register`๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ƒ์„ฑ๋œ 32-bit hex JID๋ฅผ stdout์œผ๋กœ๋งŒ ์ถœ๋ ฅ (`lib.sh:1876-1889`). `[Observed]` - `delegate_publish_event`: Non-fatal ๊ณ„์•ฝ (`|| true`). ๋ˆ„๋ฝ๋œ JID, ์Šคํฌ๋ฆฝํŠธ ๋ถ€์žฌ, ๋ธŒ๋กœ์ปค ์ „์†ก ์‹คํŒจ ์‹œ์—๋„ 0์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ํ˜ธ์ถœ์ž๋ฅผ ์ค‘๋‹จ์‹œํ‚ค์ง€ ์•Š์Œ (`lib.sh:1897-1905`). `[Observed]` - `start_watchdog`: ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ `reconcile.sh --subscribe --idle-timeout 0`๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ํ•ด๋‹น ๋ฐ๋ชฌ PID๋ฅผ stdout์œผ๋กœ ๋ฐ˜ํ™˜ (`lib.sh:1908-1938`). `[Observed]` --- ## 3. ์ด๋ฒคํŠธ ์Šคํ‚ค๋งˆ ๋ฐ ์ˆ˜๋ช…์ฃผ๊ธฐ ๊ณ„์•ฝ (MQTT & Registry) ### 3-1. ์ด๋ฒคํŠธ ์Šคํ‚ค๋งˆ v1 ๋ช…์„ธ - ํ•„์ˆ˜ ํ•„๋“œ: `schema_version` (1), `seq` (๋‹จ์กฐ ์ฆ๊ฐ€ ์–‘์˜ ์ •์ˆ˜), `job_id` (JID), `event` (์ด๋ฒคํŠธ๋ช…), `timestamp` (ISO-8601 UTC `YYYY-MM-DDTHH:MM:SSZ`), `detail` (๋ฌธ์ž์—ด ์„ค๋ช…). - ํ™•์žฅ ํ•„๋“œ: `data` (๋”•์…”๋„ˆ๋ฆฌ), `data.hmac_sig` (HMAC-SHA256 16์ง„์ˆ˜ ์„œ๋ช…). ### 3-2. ์ด๋ฒคํŠธ์™€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒํƒœ(Status) ๋งคํ•‘ ํ˜„ํ™ฉ ```mermaid stateDiagram-v2 [*] --> pending: register_job pending --> running: pick_pending / event 'started' running --> running: event 'progress' (์ƒํƒœ ๋ณ€๊ฒฝ ์—†์Œ) running --> running: event 'permission_required' (์ƒํƒœ ๋ณ€๊ฒฝ ์—†์Œ) running --> completed: event 'completed' (Terminal) running --> error: event 'error' (Terminal) running --> cancelled: event 'cancelled' / stop_session (Terminal) ``` | ์ด๋ฒคํŠธ ์ด๋ฆ„ | ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒํƒœ ์ „์ด | ์ง€์†์„ฑ(Retain) | ์„ค๋ช… | ๋ฒ”์ฃผ | |---|---|---|---|---| | `started` | `running` | `false` | ์ž‘์—…์ž๊ฐ€ ์ž‘์—…์„ ์ˆ˜๋ฝํ•˜๊ณ  ์‹คํ–‰์„ ์‹œ์ž‘ํ•จ | `[Observed]` | | `progress` | ์ „์ด ์—†์Œ (`running` ์œ ์ง€) | `false` | ์ค‘๊ฐ„ ์ง„ํ–‰ ๊ฒฝ๊ณผ ๋ณด๊ณ . ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ status๋Š” ๋ฏธ๋ณ€๊ฒฝ | `[Observed]` | | `permission_required` | ์ „์ด ์—†์Œ (`running` ์œ ์ง€) | `false` | ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์Šน์ธ ๋Œ€๊ธฐ. ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ status๋Š” ๋ฏธ๋ณ€๊ฒฝ | `[Observed]` | | `completed` | `completed` | `true` | ์ž‘์—… ์„ฑ๊ณต ์™„๋ฃŒ. Terminal ์ƒํƒœ | `[Observed]` | | `error` | `error` | `true` | ์ž‘์—… ์‹คํŒจ ์ข…๋ฃŒ. Terminal ์ƒํƒœ | `[Observed]` | | `cancelled` | `cancelled` | `true` | ์„ธ์…˜ ์ค‘๋‹จ ๋“ฑ์— ์˜ํ•œ ์ทจ์†Œ. Terminal ์ƒํƒœ | `[Observed]` | > โš ๏ธ **์ด๋ฒคํŠธ-์ƒํƒœ ๊ฐ„๊ทน Gap (`[Gap]`)**: > 1. `progress`์™€ `permission_required`๋Š” ์ด๋ฒคํŠธ๋กœ ๋ฐœํ–‰๋˜์ง€๋งŒ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `status` enum(`VALID_STATUSES = ('pending', 'running', 'completed', 'error', 'cancelled')`)์— ํฌํ•จ๋˜์ง€ ์•Š์•„, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํŒŒ์ผ๋งŒ์œผ๋กœ๋Š” ๊ถŒํ•œ ์Šน์ธ ๋Œ€๊ธฐ ์—ฌ๋ถ€๋ฅผ ์ง์ ‘ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (`registry.py:38`). > 2. `publish_event.py`์˜ Terminal ์ด๋ฒคํŠธ๋Š” `("completed", "error", "cancelled")`์ด์ง€๋งŒ, `job_subscriber.py:46`์˜ Terminal ์ด๋ฒคํŠธ๋Š” `("completed", "error")`๋กœ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ž…์ž๊ฐ€ ๋””์Šคํฌ ํด๋ฐฑ(`_check_disk_fallback`)์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๋น„๋กœ์†Œ `cancelled` ์ƒํƒœ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ๊ฐ€์ƒ `error` ์ด๋ฒคํŠธ(`synth_event="error"`)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค (`job_subscriber.py:80`). > 3. **Terminal Non-reentry (`[Proposed]` vs `[Gap]`)**: ํ•œ ๋ฒˆ Terminal ์ƒํƒœ์— ๋„๋‹ฌํ•œ ํ›„ ์ค‘๋ณต/์ƒ์ถฉ ์ด๋ฒคํŠธ์— ์˜ํ•ด ์ƒํƒœ๊ฐ€ ์—ญํ–‰ํ•˜์ง€ ์•Š๋Š” ๋ถˆ๋ณ€์‹์€ Phase 2a์˜ ๋ชฉํ‘œ ๊ณ„์•ฝ(`[Proposed]`)์ด๋ฉฐ, ํ˜„ํ–‰ `update_job_status`๋Š” ๋ฝ ๋‚ด์—์„œ ๋‹จ์ˆœ ํ•„๋“œ ๋ฎ์–ด์“ฐ๊ธฐ(`record.update(fields)`)๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ ์™„๋ฒฝํ•œ ๋น„์—ญํ–‰์ด ๋ณด์žฅ๋˜์ง€ ์•Š๋Š” ๊ฐ„๊ทน(`[Gap]`)์ด ์žˆ์Šต๋‹ˆ๋‹ค. ### 3-3. ๋ณด์•ˆ ๋ฐ ์žฌ์ƒ ๊ณต๊ฒฉ ๋ฐฉ์–ด 1. **์ธ์ฆ ๋ชจ๋“œ ๋ถ„๋ฆฌ**: `auth_token == null`์ผ ๋•Œ๋Š” HMAC ๊ฒ€์ฆ ํ†ต๊ณผ (PoC ๋ชจ๋“œ). `auth_token != null`์ผ ๋•Œ๋Š” `data.hmac_sig` ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์ฆ‰์‹œ ํ๊ธฐ (`job_subscriber.py:122-126`). `[Observed]` 2. **๋‹จ์กฐ ์‹œํ€€์Šค ๊ฒ€์ฆ**: ๊ฐ€์ž…์ž๋Š” ์ˆ˜์‹ ๋œ ํŽ˜์ด๋กœ๋“œ์˜ `seq`๊ฐ€ ์ด์ „ `last_seq`๋ณด๋‹ค ํฐ ๊ฒฝ์šฐ์—๋งŒ ์ฒ˜๋ฆฌํ•˜๊ณ , ๊ฐ™๊ฑฐ๋‚˜ ์ž‘์€ ํŒจํ‚ท์€ replay ๊ณต๊ฒฉ์œผ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ drop (`job_subscriber.py:127-136`). `[Observed]` 3. **๋กœ์ปฌ ๊ฐ์‚ฌ ์ถ”์ (Audit Log)**: MQTT ์ „์†ก ์„ฑ๊ณต/์‹คํŒจ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ `append_event`๋ฅผ ํ†ตํ•ด `.mam/delegate_job_logs//{meta,events,status}`์— ๋ชจ๋“  ์‹œ๋„ ๊ธฐ๋ก (`mqtt_common.py:122, publish_event.py:208-219`). `[Observed]` 4. **MQTT ์ „์†ก ์‹คํŒจ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ๊ธฐ๋ก**: ๋ธŒ๋กœ์ปค ์ ‘์† ๋ถˆ๊ฐ€ ์‹œ์—๋„ `publish_event.py`๋Š” ๋กœ์ปฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์˜ ์ƒํƒœ๋ฅผ `new_status`๋กœ ๋™๊ธฐํ™”ํ•œ ๋’ค exit code 2๋กœ ์ข…๋ฃŒ (`publish_event.py:223-233`). `[Observed]` --- ## 4. ๋ฆฌํฌํŠธ ํŒŒ์‹ฑ ๋ฐ ์Šน๊ฒฉ ๊ณ„์•ฝ (Report & Promotion) ### 4-1. ํŒ์ • ํ† ํฐ(Verdict) ์ถ”์ถœ ๋กœ์ง - `run_loop.sh:204-210`: `has_verdict "$file" "$verdict"`๋Š” ํŒŒ์ผ์˜ **๋งˆ์ง€๋ง‰ ๋น„๊ณต๋ฐฑ ํ–‰(last non-blank line)**์„ ์ถ”์ถœํ•˜์—ฌ `^\[VERDICT: ${verdict}\][[:space:]]*\r?$` ํŒจํ„ด๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. - `[Gap]`: ๋งˆ์ง€๋ง‰ ํ–‰์ด ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก(```) ๋‚ด๋ถ€์— ์žˆ๋Š” ์˜ˆ์ œ ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ์—๋„ ํ˜„์žฌ ์…ธ ์ •๊ทœ์‹์€ ์ด๋ฅผ ์œ ํšจํ•œ ํŒ์ • ํ† ํฐ์œผ๋กœ ์˜ค์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Phase 2์˜ ์—„๊ฒฉํ•œ ํŒŒ์„œ(Strict Parser)๋Š” ์ฝ”๋“œ ํŽœ์Šค ๋‚ด๋ถ€๋ฅผ ์ œ์™ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - ๋ฆฌํฌํŠธ ํŒŒ์ผ ์œ„์น˜ ํƒ์ƒ‰(`find_report`): `find ".mam/jobs/$1" -maxdepth 2 -name "report-final.md" | head -n 1`๋กœ ์ฒซ ๋ฒˆ์งธ ์ผ์น˜ ํŒŒ์ผ์„ ์ทจํ•˜๋ฉฐ, ์ •ํ™•ํ•œ ๋‹จ์ผ ๊ฒฝ๋กœ ๊ฐ•์ œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค (`run_loop.sh:234`). `[Observed]` ### 4-2. ์Šน๊ฒฉ(Promotion) ๋ฐ ์ •๋ฆฌ(Cleanup) ๊ณ„์•ฝ 1. **๊ณ„ํš์„œ ์Šน๊ฒฉ**: `CURRENT_PLAN`์„ `.agents/reports/$PLANNER_SESSION/plan-${PLAN_JOB_ID}.md.tmp`์— ๊ธฐ๋ก ํ›„ `mv -f`๋กœ ์›์ž์  ๊ต์ฒด (`run_loop.sh:895-901`). `[Observed]` 2. **๋ฆฌ๋ทฐ ๋ฆฌํฌํŠธ ์Šน๊ฒฉ**: ๊ฐ ๋ฆฌ๋ทฐ์–ด์˜ `report-final.md`๋ฅผ `.agents/reports/$rev/report-${job_id}.md`๋กœ ๋ณต์‚ฌ ๋ฐ ์ด๋™ (`run_loop.sh:903-916`). `[Observed]` 3. **๋ฐฐ์น˜ ๋น„์›์ž์„ฑ (`[Gap]`)**: ๋‹ค์ˆ˜์˜ ๋ฆฌ๋ทฐ ๋ฆฌํฌํŠธ๋ฅผ ์Šน๊ฒฉํ•  ๋•Œ ๊ฐœ๋ณ„ ํŒŒ์ผ ๋‹จ์œ„๋กœ `cp && mv`๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ, ์ค‘๊ฐ„ ํฌ๋ž˜์‹œ ๋ฐœ์ƒ ์‹œ ์ผ๋ถ€ ๋ฆฌํฌํŠธ๋งŒ ์Šน๊ฒฉ๋˜๋Š” ๋ถ€๋ถ„ ์„ฑ๊ณต ์œ„ํ—˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. --- ## 5. ์ž ๊ธˆ(Lock) ๋ฐ ๋™์‹œ์„ฑ ์ œ์–ด ๊ณ„์•ฝ ### 5-1. ๋ฝ ์ฒด๊ณ„ ๋ฐ ํš๋“ ์ˆœ์„œ 1. **Workspace Loop Guard**: `.mam/loop-guard-active` (์›์ž์  ํ•˜๋“œ๋งํฌ + `pid`/`lstart` ์Šคํ‹ธ ์žฌ์‹œ๋„). 2. **Session Registry Lock**: `.mam/agent-sessions.db` (SQLite WAL ๋™์‹œ์„ฑ ๋ฐ ํŠธ๋žœ์žญ์…˜). 3. **Job Registry Lock**: `.mam/jobs/.lock` ๋˜๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฝ (`fcntl.flock(LOCK_EX)`). ### 5-2. `update_job_status`์˜ ๋น„-CAS ํŠน์„ฑ (`[Gap]`) - ํ˜„์žฌ `mqtt_common.py:377-403`์˜ `update_job_status`๋Š” ๋ฝ์„ ํš๋“ํ•œ ํ›„ `record.update(fields)`๋กœ ๋‹จ์ˆœํžˆ ํ•„๋“œ๋ฅผ ๋ฎ์–ด์“ฐ๋ฉฐ, ํ˜ธ์ถœ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•œ ์ด์ „ ์ƒํƒœ(`expected_status`)๋‚˜ attempt ๋ฒ„์ „์„ ๋น„๊ตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - ๋˜ํ•œ `registry.py:129-131`์˜ `pick_pending`์€ `update_job_status`๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์ง์ ‘ ํŒŒ์ผ์— `chosen["status"] = "running"`์„ ์”๋‹ˆ๋‹ค. - **`[Proposed]`**: Phase 2a์—์„œ๋Š” ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ์ด ๋‹จ์ผ ์—”์ง„์˜ ์›์ž์  CAS(Compare-And-Swap) ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ˆ˜ํ–‰๋˜๋„๋ก ํ†ต์ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. --- ## 6. ์†Œ์Šค ๋ฐ ํ…Œ์ŠคํŠธ ๋งคํ•‘ ์š”์•ฝ | ๊ตฌ์„ฑ์š”์†Œ | ํ•ต์‹ฌ ์†Œ์Šค ํŒŒ์ผ | ํ•ต์‹ฌ ํ…Œ์ŠคํŠธ ํŒŒ์ผ | ๊ธฐ์ค€์„  ์ƒํƒœ | |---|---|---|---| | ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ CLI | `run_loop.sh` | `tests/test_loop_cli.py` | 9๊ฐœ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | | ๋ฃจํ”„ ๊ฐ€๋“œ ๋ฝ | `loop_lock.sh` | `tests/test_o2_race_free_lock.py` | 22๊ฐœ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | | ๋ฐ˜๋ก  ์ƒํ•œ์„  | `run_loop.sh` | `tests/test_o1_rebuttal.py` | 10๊ฐœ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | | ์œ„์ž„ ํ—ฌํผ | `lib.sh` | `tests/test_tier1_unit.py` | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | | ์ž‘์—… ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ | `registry.py`, `mqtt_common.py` | `tests/test_tier2_component.py` | fcntl ๊ธฐ๋ฐ˜ ๋ฝํ‚น |