# ๐Ÿ” Review Report โ€” PR #38 ๋ธํƒ€ ๋ฆฌ๋ทฐ 2ํšŒ์ฐจ (job `895b0370`) - **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) ยท 2026-09-25 - **๋Œ€์ƒ ๋ธํƒ€**: `ed293dc..e80186f` (2์ปค๋ฐ‹, 3ํŒŒ์ผ `+73/โˆ’10`, `git diff --check` ํด๋ฆฐ), base `main 7a4a0c2` (v4.7.3). - **๊ฒ€์ฆ ๊ฒฝ๋กœ**: `/var/folders/.../T/mam-review/loop-20260925T072328Z-2f5853/1`. **identity ํ™•์ธ**: `HEAD = e80186f5โ€ฆ4774`, `HEAD^{tree} = 35018688โ€ฆ7a72` โ€” ๋ธŒ๋ฆฌํ”„์™€ ์ผ์น˜. - **1ํšŒ์ฐจ ๋‚ด ํŒ์ •**: PASS + K1~K4 (job `107d435d`). ์ด๋ฒˆ์—๋Š” N1/R1(cancelled ์‚ฌ์œ )ยทR2(์›๊ฒฉ ๋งˆ๊ฐ ๋ฌธ๊ตฌ)ยทK1(STRAY_RULE)์˜ ์ •์ •์„ ๋ณธ๋‹ค. - **์•ˆ์ „ ๊ทœ์น™ ์ค€์ˆ˜**: ๊ฒ€์ฆ ๊ฒฝ๋กœยท์†Œ์Šค ์›ŒํฌํŠธ๋ฆฌยท๋ณธ ์ฒดํฌ์•„์›ƒ(`main 7a4a0c2`) ๋ชจ๋‘ ์ˆ˜์ •ยท์ปค๋ฐ‹ยทcheckout ์—†์Œ. ์‹คํ—˜์€ in-process ๋Ÿฌ๋„ˆ(`env -u MAM_VERSION`, ์ž„์‹œ ๋“ฑ๋ก๋ถ€ยท๊ฐ์‚ฌ ๋กœ๊ทธ)์™€ ๋˜๋Œ๋ฆฐ ์‚ฌ๋ณธ์—์„œ๋งŒ. ์‹ค์ œ herdrยท์„ธ์…˜ยท`.mam`ยท๋ฐ๋ชฌ ์กฐ์ž‘ ์—†์Œ, pushยทmergeยทPR ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์Œ. ## ๊ฒฐ๋ก  ์š”์•ฝ **์„ธ ์ •์ • ๋ชจ๋‘ ์ง€์ ์„ ๋‹ซ๊ณ , 1ํšŒ์ฐจ ํŒ์ •์„ ๋’ค์ง‘์„ ์š”์†Œ๋Š” ์—†๋‹ค.** ์ข…๊ฒฐ ์‚ฌ์œ  12๊ฐ€์ง€๋ฅผ `_subjob_state` ์— ๋„ฃ์–ด ๋ถ„๋ฅ˜๊ฐ€ ์„ค๊ณ„๋Œ€๋กœ์ž„์„ ํ™•์ธํ–ˆ๊ณ (๊ฐ์‚ฌ ndjson ๊ฒฝ๋กœยท`.events.log` ํด๋ฐฑ ์–‘์ชฝ), `get_feedback` ์˜ ๊ธฐ๋ณธ ๋™์ž‘์€ ๊ทธ๋Œ€๋กœ๋‹ค. ๋˜๋Œ๋ฆฐ ์‚ฌ๋ณธ์—์„œ T13ร—2ยทT14ยทT6 ๋งŒ ์‹คํŒจํ•œ๋‹ค. ๋น„์ฐจ๋‹จ ๊ด€์ฐฐ 1๊ฑด(L1). ## 1. N1/R1 โ€” `cancelled` ์‚ฌ์œ  (ํ•ญ๋ชฉ 1) - `registry.get_feedback(job_id, registry_dir, events=("completed","error"))` (`registry.py:316-317`): ๊ธฐ๋ณธ๊ฐ’์ด ์ข…์ „๊ณผ ๊ฐ™์•„ ํ˜ธ์ถœ์ž โ€” ๋ž˜ํผ์˜ `get-feedback` CLI 3๊ณณ(`:556`,`:565`,`:593`)๊ณผ `registry.py get-feedback` ์„œ๋ธŒ์ปค๋งจ๋“œ(`:668`) โ€” ๋Š” ์ธ์ž ์—†์ด ๋ถ€๋ฅด๋ฏ€๋กœ **๋™์ž‘ ๋ถˆ๋ณ€**. ์‹ค์ธก: `error "first"` ๋’ค `cancelled "session_busy"` ๊ฐ€ ์˜จ ์žก์—์„œ ๊ธฐ๋ณธ ํ˜ธ์ถœ์€ `'first'`, `events=(โ€ฆ,"cancelled")` ๋Š” `'session_busy'`. - `_subjob_state`(`runner.py:684-702`)๋งŒ `cancelled` ๋ฅผ ํฌํ•จํ•ด ์ฝ๋Š”๋‹ค(`:691`). - ๋‘ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ™•์ธ: `.events.log` ์—๋งŒ `cancelled` ๊ฐ€ ์žˆ๋Š” ์žก โ†’ `NOT-DISPATCHED/target_not_found`; **๊ฐ์‚ฌ ndjson ์—๋งŒ**(`.events.log` ์—†์Œ, `source_event: cancelled`) ์žˆ๋Š” ์žก โ†’ `NOT-DISPATCHED/session_busy`. ## 2. R2 โ€” ์ฒซ ๋‹จ์–ด ํŒ์ • (ํ•ญ๋ชฉ 1) `closed_kind = reason.split(" ", 1)[0]`(`:696`) ์„ `ORCHESTRATOR_CLOSED` ์™€ ๋น„๊ตํ•œ๋‹ค. ์›๊ฒฉ ๋Ÿฌ๋„ˆ์˜ ์‹ค์ œ ๋ฌธ๊ตฌ๋Š” `remote.py:352` ์˜ `"deadline_timeout (remote runner)"` ๋‹ค. ์˜ค๋ถ„๋ฅ˜ ์—ฌ๋ถ€๋ฅผ ์‚ฌ์œ  12๊ฐ€์ง€๋กœ ์‹ค์ธก: | ์ƒํƒœยท์‚ฌ์œ  | ๋ถ„๋ฅ˜ | |---|---| | `error` `deadline_timeout` (๋ž˜ํผ) | CLOSED-UNCONFIRMED | | `error` `deadline_timeout (remote runner)` | **CLOSED-UNCONFIRMED** (R2 ์ •์ •) | | `error` `agent_blocked` | CLOSED-UNCONFIRMED | | `error` ์—์ด์ „ํŠธ ์ž์‹ ์˜ ์‚ฌ์œ (`tests failed on 3 cases`) | `error`(์‹คํŒจ) โ€” ์ฒซ ๋‹จ์–ด `tests` ๋Š” ํŠœํ”Œ ๋ฐ– | | `error` ์‚ฌ์œ  ์—†์Œ | CLOSED-UNCONFIRMED (๊ธฐ์กด ๊ทœ์น™) | | `cancelled` `target_not_found` / `session_busy` / `rolled back by create_session` | **NOT-DISPATCHED** (N1 ์ •์ •; `session_busy` ๋Š” ์ฒซ ๋‹จ์–ด ํŒ์ •์— ๊ฑธ๋ฆฌ์ง€ ์•Š๊ณ  cancelled ๋ถ„๊ธฐ๋กœ ๊ฐ„๋‹ค) | | `cancelled` ์‚ฌ์œ  ์—†์Œ / `operator abort` | `cancelled`(์‹คํŒจ ์ง‘๊ณ„) | | `completed` `all done` | completed | ์ฒซ ๋‹จ์–ด ํŒ์ •์ด `session_busy`ยท`target_not_found`ยท`rolled` ๋“ฑ์„ ๋ฏธํ™•์ธ ์ข…๊ฒฐ๋กœ ์˜ค๋ถ„๋ฅ˜ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‚จ๋Š” ์ด๋ก ์  ์—ฌ์ง€๋Š” ์—์ด์ „ํŠธ๊ฐ€ **์ž๊ธฐ ์‚ฌ์œ ๋ฅผ `deadline_timeout`/`agent_blocked` ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฌธ์žฅ์œผ๋กœ ์“ฐ๋Š” ๊ฒฝ์šฐ**๋ฟ์ธ๋ฐ, ์ด ๋‘ ๋‹จ์–ด๋Š” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์˜ˆ์•ฝ์–ด๋ผ ์‹ค๋ฌด์—์„œ ๊ฒน์น˜์ง€ ์•Š๋Š”๋‹ค(L1, ์ •๋ณด). ## 3. K1 โ€” `STRAY_RULE` (ํ•ญ๋ชฉ 1) `runner.py:41-42` ์˜ ๋ฌธ์žฅ์€ "๊ท€์†๋˜์ง€ ์•Š์€ ์—ด๋ฆฐ ์žก์€ `--parent-job` ์—†์ด ๋“ฑ๋ก๋œ ์ด ์ž‘์—…์˜ ํ•˜์œ„ ์œ„์ž„์ผ ์ˆ˜ ์žˆ๋‹ค; ๊ทธ ์‚ฐ์ถœ๋ฌผ์ด ์ž‘์—… ๋ชฉํ‘œ์— ํ•„์š”ํ•˜๋ฉด NOT PASS" ์ด๊ณ , `_subjobs_block` ์€ stray ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ stray ์ค„ ๋’ค์— ๋ถ™์ธ๋‹ค(`:727-729`). SUBJOB_RULE(ํ•˜์œ„ ์žก์ด ์žˆ์œผ๋ฉด ๋Œ€์ฒด ์™„๋ฃŒ ์ž…์ฆ์ด ์—†๋Š” ํ•œ NOT PASS)๊ณผ **๋ชจ์ˆœ๋˜์ง€ ์•Š๋Š”๋‹ค** โ€” stray ๋Š” ๋ฌด๊ด€ํ•œ ์žก์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ "ํ•„์š”ํ•˜๋ฉด" ์ด๋ผ๋Š” ์กฐ๊ฑด๋ถ€๊ฐ€ ๋งž๊ณ , ๋‘˜ ๋‹ค ๊ฐ™์€ ๋ฐฉํ–ฅ(๋ฏธ์™„ โ†’ NOT PASS)์ด๋‹ค. stray ๊ฐ€ ์—†์œผ๋ฉด ํ”„๋กฌํ”„ํŠธ๋Š” 1ํšŒ์ฐจ์™€ ๋ฐ”์ดํŠธ ๋™์ผ(`if not rows and not stray: return ""` ์œ ์ง€). ## 4. ํ…Œ์ŠคํŠธ (ํ•ญ๋ชฉ 1ยท2) ### 4-1. ์ˆ˜์ • ์—†์ด๋Š” ์‹คํŒจํ•˜๋Š”๊ฐ€ โ€” ํ™•์ธํ•จ ๊ฒ€์ฆ ๊ฒฝ๋กœ ์‚ฌ๋ณธ์—์„œ `registry.py`ยท`runner.py` ๋งŒ `ed293dc` ํŒ์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ณ  `-k "t13 or t14 or t6 or t5"` ๋ฅผ ๋Œ๋ ธ๋‹ค: ``` 4 failed, 3 passed, 13 deselected in 22.39s FAILED test_t13_โ€ฆ[target_not_found] โ† N1 FAILED test_t13_โ€ฆ[session_busy] โ† N1 FAILED test_t14_the_remote_runners_deadline_wording_is_closed_unconfirmed โ† R2 FAILED test_t6_an_unattributed_open_job_is_shown_and_waited_for_only_on_request โ† K1 (์ƒˆ ๋‹จ์–ธ) ``` T5/T5b(`deadline_timeout` ์ •ํ™• ์ผ์น˜ยท๋Šฆ์€ ์“ฐ๊ธฐ ๋ฌดํšจํ™”)๋Š” ์–‘์ชฝ์—์„œ ํ†ต๊ณผ โ€” ๊ธฐ์กด ๋ถ„๋ฅ˜๊ฐ€ ์ •์ •์œผ๋กœ ๋ฐ”๋€Œ์ง€ ์•Š์•˜๋‹ค. T13ยทT14 ๋Š” ์‹ค์ œ `registry.py close` + `publish_event` ๋กœ ์ข…๊ฒฐ์„ ๋งŒ๋“œ๋Š” ์‹œํ—˜์ด๋ผ(mock ์•„๋‹˜) ์›๊ฒฉ ๋Ÿฌ๋„ˆ์˜ ์‹ค์ œ ๋ฌธ๊ตฌ ๊ทธ๋Œ€๋กœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. ### 4-2. ๊ฒ€์ฆ ๊ฒฝ๋กœ์—์„œ์˜ ์‹คํ–‰ ``` tests/test_issue34_job_tree.py tests/test_issue5_phase1_delegation.py tests/test_issue14_delivery_ack.py tests/test_loop_cli.py tests/test_issue25_plan_reuse.py tests/test_issue21_loop_b.py โ†’ 158 passed in 271.04s failed 0 ยท skipped 0 ``` ์ƒˆ ํŒŒ์ผ 20๊ฑด ์ „๋ถ€ ํ†ต๊ณผ, `registry.py` CLI ์Šค๋ƒ…์ƒทยทhelp ์บก์ฒ˜(issue5 phase1) ํ†ต๊ณผ โ†’ `get_feedback` ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์žฅ์ด CLI ๊ณ„์•ฝ์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. ์‹ค์ œ `.mam/jobs` ์ „ํ›„ **์ฐจ์ด 0**, ๊ฒ€์ฆ ๊ฒฝ๋กœ tracked ํŒŒ์ผ ๋ณ€ํ™” ์—†์Œ(HEAD `e80186f`), ๋ณธ ์ฒดํฌ์•„์›ƒ `main 7a4a0c2`. ## 5. ๋ณ‘ํ•ฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ (ํ•ญ๋ชฉ 2) 1ํšŒ์ฐจ PASS ๋ฅผ ๋’ค์ง‘์„ ์š”์†Œ๋Š” ์—†๋‹ค. ๋ธํƒ€๋Š” `get_feedback` ์˜ ๊ธฐ๋ณธ๊ฐ’ ๋ณด์กด ์ธ์ž ํ•˜๋‚˜, `_subjob_state` ์˜ ๋‘ ์ค„, `STRAY_RULE` ํ•œ ๋ฌธ์žฅ, ํšŒ๊ท€ 3๊ฑด์ด๋ฉฐ, ์ •์ • ๋Œ€์ƒ๋งŒ ์ •ํ™•ํžˆ ์‹คํŒจโ†’ํ†ต๊ณผ๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค. ๋ฒ„์ „ ๋ฝ์Šคํ… bump ๋ถ€์žฌ(`lib.sh:32` `4.7.3`, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ 4.7.4 ๋กœ ์ฒ˜๋ฆฌ ์˜ˆ์ •) ์™ธ์— **๋ณ‘ํ•ฉ์„ ๋ง‰์„ ๊ฒฐํ•จ์€ ์—†๋‹ค**. 1ํšŒ์ฐจ K2(`MAM_JOB_TREE_MAX` ์ดˆ๊ณผ ์‹œ ๋Œ€๊ธฐ ์ƒ๋žต)ยทK3(`target_not_found` ์˜ rc 5)ยทK4(ํด๋ง๋งˆ๋‹ค ์ „์ฒด ์Šค์บ”)๋Š” ์ด ๋ธํƒ€ ๋ฒ”์œ„ ๋ฐ–์ด๋ผ ๊ทธ๋Œ€๋กœ ์—ด๋ ค ์žˆ๋‹ค. | # | ๋น„์ฐจ๋‹จ ๊ด€์ฐฐ | ๊ทผ๊ฑฐ | |---|---|---| | L1 | ์ฒซ ๋‹จ์–ด ํŒ์ •์€ ์—์ด์ „ํŠธ๊ฐ€ ์ž๊ธฐ ์‚ฌ์œ ๋ฅผ `deadline_timeout`/`agent_blocked` ๋กœ ์‹œ์ž‘ํ•ด ์“ฐ๋ฉด ๋ฏธํ™•์ธ ์ข…๊ฒฐ๋กœ ๋ณธ๋‹ค โ€” ๋‘ ๋‹จ์–ด๋Š” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์˜ˆ์•ฝ์–ด๋ผ ์‹ค๋ฌด ์ถฉ๋Œ์€ ์—†๋‹ค | `runner.py:696`, ยง2 ํ‘œ | ## 6. ๊ต์ฐจ๊ฒ€์ฆ ๊ฐ€์ด๋“œ ```bash RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T072328Z-2f5853/1 PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # e80186fโ€ฆ / 3501868โ€ฆ # in-process (env -u MAM_VERSION): ์ž„์‹œ ๋“ฑ๋ก๋ถ€์— 12๊ฐ€์ง€ ์ข…๊ฒฐ ์‚ฌ์œ ๋ฅผ .events.log / ๊ฐ์‚ฌ ndjson ์œผ๋กœ ์‹ฌ๊ณ  _subjob_state ํ˜ธ์ถœ (ยง2 ํ‘œ) # ๋˜๋Œ๋ฆฐ ์‚ฌ๋ณธ: registry.pyยทrunner.py ๋ฅผ ed293dc ํŒ์œผ๋กœ โ†’ -k "t13 or t14 or t6 or t5" โ†’ 4 failed / 3 passed (cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -p no:cacheprovider -q --basetemp=/tmp/bt tests/test_issue34_job_tree.py) # 20 passed ``` [VERDICT: PASS]