diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-f380eb54.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-f380eb54.md new file mode 100644 index 0000000..c77eb43 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-f380eb54.md @@ -0,0 +1,389 @@ +# ๐Ÿ“ ๊ตฌํ˜„ ๊ณ„ํš์„œ Rev.2 โ€” B-9 (P4-1): `LOGS_DIR` import ์‹œ์  cwd ๊ณ ์ • ํ•ด์†Œ + +- **Job ID**: `7248c715` (Rev.1 = `f380eb54`) +- **Planner**: claude (session: `herdr:canary-projects-multi-agent-mux-creator-claude`) +- **Role**: Planner (`MULTI_AGENT_RULES.md` ยง1 โ€” ๋ณธ ์ž‘์—…์—์„œ ์ €์žฅ์†Œ ์ฝ”๋“œ 0๊ฑด ์ˆ˜์ •) +- **๋ฐ˜์˜ ๋Œ€์ƒ Challenge**: `07b5bd28` (agy, Worker / Plan Reviewer) โ€” `[VERDICT: PASS WITH CHALLENGE]` +- **๊ธฐ์ค€ ์ปค๋ฐ‹**: `8cee937` (`refactor`, ์ž‘์—… ํŠธ๋ฆฌ clean) + +--- + +## 0. ์š”์•ฝ + +Challenge 2๊ฑด์„ **์‹ค์ธก์œผ๋กœ ํŒ์ •**ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ๊ฐˆ๋ฆฝ๋‹ˆ๋‹ค. + +| # | ์ง€์  | ํŒ์ • | ๊ทผ๊ฑฐ | +|---|---|---|---| +| **C1** | macOS `/var`โ†’`/private/var` ์‹ฌ๋งํฌ๋กœ ยง5.1 ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจ | โš ๏ธ **์ผ๋ฐ˜๋ก ์€ ์˜ณ์œผ๋‚˜ ์ด ํ…Œ์ŠคํŠธ์—๋Š” ๋ฏธํ•ด๋‹น โ€” ๊ฒฐ๋ก  ๊ธฐ๊ฐ** | pytest `tmp_path` ๋Š” **์ด๋ฏธ resolve ๋œ** `/private/var/โ€ฆ` ๋ฅผ ๋ฐ˜ํ™˜. ์‹ค์ธก `naive == : True` | +| **C1'** | (๊ทธ๋Ÿผ์—๋„) `realpath` ์ •๊ทœํ™” ์ ์šฉ | โœ… **์ฑ„ํƒ โ€” ๋‹จ, ์‚ฌ์œ ๋ฅผ ์ •์ •** | "์ง€๊ธˆ ๊นจ์ง€๋ฏ€๋กœ"๊ฐ€ ์•„๋‹ˆ๋ผ "pytest ๋‚ด๋ถ€ `.resolve()` ์— ๋Œ€ํ•œ **์•”๋ฌต์  ์˜์กด**์„ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ" | +| **C2-a** | PEP 562 ์— `__dir__()` ๋™๋ฐ˜ ์ •์˜ | โœ… **์ฑ„ํƒ โ€” ๋‹จ, ์ฃผ์žฅ ์ผ๋ถ€ ์ •์ •** | `dir()` ์—๋Š” ์˜ํ–ฅ ์žˆ์Œ(Falseโ†’True). **`hasattr` ์€ `__dir__` ์—†์ด๋„ True**(์‹ค์ธก) | +| **C2-b** | AST ๊ฐ€๋“œ์— `ast.AnnAssign` ์ถ”๊ฐ€ | โœ… **์ „๋ฉด ์ฑ„ํƒ** | `LOGS_DIR: str = โ€ฆ` ๋Š” `AnnAssign` ์œผ๋กœ ํŒŒ์‹ฑ๋˜์–ด ํ˜„ ๊ฐ€๋“œ๊ฐ€ **์™„์ „ํžˆ ๋†“์นจ**(์‹ค์ธก) | + +๊ทธ๋ฆฌ๊ณ  ์ฑŒ๋ฆฐ์ €์˜ `__dir__` ๊ตฌํ˜„์•ˆ ์ž์ฒด์—์„œ **๊ฒฝ๋ฏธํ•œ ๊ฒฐํ•จ 1๊ฑด**์„ ์ฐพ์•˜๊ณ , Rev.1 ๊ฐ€๋“œ์˜ **์•ฝํ•œ ๋‹จ์–ธ 1๊ฑด**์„ ์Šค์Šค๋กœ ๋ฐœ๊ฒฌํ•ด ๋ณด๊ฐ•ํ–ˆ์Šต๋‹ˆ๋‹ค. + +ยง1~ยง4(๊ฒฐํ•จ ์ง„๋‹จ, T1ยทT2ยทT3 ํ•จ์ •, ์„ค๊ณ„, ํ•˜์œ„ ํ˜ธํ™˜ ๋ถ„์„)๋Š” ์ฑŒ๋ฆฐ์ €๊ฐ€ ยง3 ํ‘œ์—์„œ ์ „๋ถ€ "Proceed as planned" ๋กœ ํ‰๊ฐ€ํ–ˆ์œผ๋ฏ€๋กœ **๋ณ€๊ฒฝ ์—†์ด ์œ ์ง€**ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 1. C1 ํŒ์ • โ€” ์ผ๋ฐ˜๋ก  ์ˆ˜์šฉ, ๊ฒฐ๋ก  ๊ธฐ๊ฐ (์‹ค์ธก) + +### 1.1 ์ฑŒ๋ฆฐ์ €์˜ ์žฌํ˜„์€ ์œ ํšจํ•˜๋‹ค โ€” ๋‹ค๋งŒ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋‹ค + +์ฑŒ๋ฆฐ์ €๋Š” `tempfile.gettempdir()` ๋กœ ์žฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ฒฝ๋กœ๋Š” ์‹ค์ œ๋กœ ๋ฏธํ•ด๊ฒฐ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. + +``` +tempfile.gettempdir(): /var/folders/q_/โ€ฆ/T +realpath : /private/var/folders/q_/โ€ฆ/T +differ? : True โ† ์ฑŒ๋ฆฐ์ € ๊ด€์ฐฐ ์ •ํ™• +``` + +### 1.2 ๊ทธ๋Ÿฌ๋‚˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์“ฐ๋Š” `tmp_path` ๋Š” ์ด๋ฏธ resolve ๋˜์–ด ์žˆ๋‹ค + +์ œ์•ˆ ํ…Œ์ŠคํŠธ๋Š” `tempfile` ์ด ์•„๋‹ˆ๋ผ pytest ์˜ `tmp_path` ํ”ฝ์Šค์ฒ˜๋ฅผ ์”๋‹ˆ๋‹ค. ์‹ค์ œ ํ”ฝ์Šค์ฒ˜๋กœ ์ธก์ •ํ•œ ๊ฒฐ๊ณผ: + +``` +tmp_path : /private/var/folders/q_/โ€ฆ/T/pytest-of-godopu16/pytest-156/test_c1_symlink_premise0 +str(a) : /private/var/folders/โ€ฆ/test_c1_symlink_premise0/a +os.getcwd() : /private/var/folders/โ€ฆ/test_c1_symlink_premise0/a +naive == : True โ† Rev.1 ํ…Œ์ŠคํŠธ๋Š” ๊ทธ๋Œ€๋กœ ํ†ต๊ณผํ•œ๋‹ค +realpath == : True +``` + +pytest ์˜ `TempPathFactory` ๋Š” base temp ๋ฅผ `.resolve()` ํ•˜๋ฏ€๋กœ `tmp_path` ์–‘๋ณ€์ด ๋ชจ๋‘ ํ•ด๊ฒฐ๋œ ์ƒํƒœ์ด๊ณ , `os.getcwd()` ๋„ ํ•ญ์ƒ ํ•ด๊ฒฐ๋œ ๊ฒฝ๋กœ๋ฅผ ๋Œ๋ ค์ค๋‹ˆ๋‹ค. **๋”ฐ๋ผ์„œ Rev.1 ํ…Œ์ŠคํŠธ๋Š” macOS ์—์„œ ์‹คํŒจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** + +### 1.3 ๊ทธ๋Ÿผ์—๋„ ์ •๊ทœํ™”๋ฅผ ์ฑ„ํƒํ•˜๋Š” ์ด์œ  (์‚ฌ์œ  ์ •์ •) + +"์ง€๊ธˆ ๊นจ์ง„๋‹ค"๋Š” ๊ทผ๊ฑฐ๋Š” ์„ฑ๋ฆฝํ•˜์ง€ ์•Š์ง€๋งŒ, **์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค.** ์‚ฌ์œ ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. + +- ํ˜„์žฌ ํ†ต๊ณผ๋Š” **pytest ๋‚ด๋ถ€ ๊ตฌํ˜„(`.resolve()`)์— ๋Œ€ํ•œ ์•”๋ฌต์  ์˜์กด**์ž…๋‹ˆ๋‹ค. ๋ฌธ์„œํ™”๋œ ๊ณ„์•ฝ์ด ์•„๋‹™๋‹ˆ๋‹ค. +- ๋ˆ„๊ตฐ๊ฐ€ ๋‚˜์ค‘์— `tempfile.mkdtemp()` ๋‚˜ ์‹ฌ๋งํฌ๋œ ๋””๋ ‰ํ„ฐ๋ฆฌ๋กœ ๋ฐ”๊พธ๋ฉด ์กฐ์šฉํžˆ ๊นจ์ง‘๋‹ˆ๋‹ค โ€” ๊ทธ๋•Œ์˜ ์‹คํŒจ ๋ฉ”์‹œ์ง€๋Š” B-9 ์™€ ๋ฌด๊ด€ํ•ด ๋ณด์—ฌ ๋””๋ฒ„๊น… ๋น„์šฉ์ด ํฝ๋‹ˆ๋‹ค. +- `os.path.realpath` ๋Š” ์–‘๋ณ€์— ๋ถ™์—ฌ๋„ **๋น„์šฉ 0**์ด๊ณ  ์˜์กด์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + +> **๋ถ€์ˆ˜ ํ™•์ธ โ€” ๋‚˜๋จธ์ง€ ๊ฐ€๋“œ๋Š” ์˜ํ–ฅ ์—†์Œ**: `test_b9_audit_log_lands_under_the_current_cwd` ๋Š” `Path.exists()` ๋กœ ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค. `/var/โ€ฆ` ์™€ `/private/var/โ€ฆ` ๋Š” ๊ฐ™์€ ๋Œ€์ƒ์œผ๋กœ ํ•ด์„๋˜๋ฏ€๋กœ ์‹ฌ๋งํฌ์™€ ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค(์‹ค์ธก `exists() via tmp_path: True`). ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ€๋“œ๋Š” `os.getcwd()` ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์•„ ์• ์ดˆ์— ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค. **C1 ์€ ๋ฌธ์ž์—ด ๋น„๊ต ๊ฐ€๋“œ 1๊ฑด์—๋งŒ ํ•ด๋‹น**ํ•˜๋ฉฐ, ์ฑŒ๋ฆฐ์ €๊ฐ€ ๊ทธ ๋ฒ”์œ„๋ฅผ ์ •ํ™•ํžˆ ์งš์—ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## 2. C2 ํŒ์ • โ€” ์ฑ„ํƒ, ๋‘ ๊ณณ ์ •์ • + +### 2.1 `__dir__()` โ€” ์ฑ„ํƒ, ๋‹จ `hasattr` ์ฃผ์žฅ์€ ์‚ฌ์‹ค๊ณผ ๋‹ค๋ฆ„ + +์‹ค์ธก: + +``` +no __dir__ : 'LOGS_DIR' in dir() -> False | hasattr -> True | getattr ๋™์ž‘ -> True +with __dir__ : 'LOGS_DIR' in dir() -> True | hasattr -> True +``` + +- `dir()` ์—์„œ ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ์€ **๋งž์Šต๋‹ˆ๋‹ค**(Falseโ†’True). ๋Œ€ํ™”ํ˜• ๋„๊ตฌยทํƒญ ์™„์„ฑ์— ์˜ํ–ฅ์ด ์žˆ์œผ๋ฏ€๋กœ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค. +- ๊ทธ๋Ÿฌ๋‚˜ **`hasattr` ์€ `__dir__` ์—†์ด๋„ True** ์ž…๋‹ˆ๋‹ค. `hasattr` ์€ `getattr` ์„ ๊ฑฐ์น˜๋ฏ€๋กœ `__getattr__` ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ์ฑŒ๋ฆฐ์ € ยง1-2 ์˜ "`dir()`, `hasattr`, ๋Œ€ํ™”ํ˜• ๋„๊ตฌ์—์„œ ๋ฐœ๊ฒฌ ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ณด์žฅ"์ด๋ผ๋Š” ์„œ์ˆ  ์ค‘ `hasattr` ๋ถ€๋ถ„์€ ์ •์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค โ€” ์˜คํ•ดํ•˜๋ฉด "`__dir__` ์ด ์—†์œผ๋ฉด `hasattr` ์ด ๊นจ์ง„๋‹ค"๊ณ  ์ฝํž™๋‹ˆ๋‹ค. + +### 2.2 ์ฑŒ๋ฆฐ์ €์˜ `__dir__` ๊ตฌํ˜„์•ˆ์— ์ค‘๋ณต ๊ฒฐํ•จ + +๊ถŒ๊ณ ์•ˆ: + +```python +def __dir__(): + return sorted(list(globals().keys()) + ["LOGS_DIR"]) +``` + +์ „์—ญ `LOGS_DIR` ์ด ๋˜์‚ด์•„๋‚œ ์ƒํƒœ์—์„œ ์‹ค์ธก: + +``` +proposed : LOGS_DIR count in dir() = 2 โ† ์ค‘๋ณต +set-based : LOGS_DIR count = 1 +``` + +ํ•˜ํ•„ **T1 ํšŒ๊ท€๊ฐ€ ์ผ์–ด๋‚œ ์ƒํƒœ**(์ „์—ญ ์žฌ๋„์ž…)์—์„œ ์ค‘๋ณต์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ๊ทธ ์ƒํ™ฉ์„ ๋””๋ฒ„๊น…ํ•˜๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ํ˜ผ๋ž€์„ ์ฃผ๋ฏ€๋กœ ์ง‘ํ•ฉ ๊ธฐ๋ฐ˜์œผ๋กœ ์”๋‹ˆ๋‹ค. + +```python +def __dir__(): + return sorted(set(globals()) | {"LOGS_DIR"}) +``` + +### 2.3 `ast.AnnAssign` โ€” ์ „๋ฉด ์ฑ„ํƒ + +``` +LOGS_DIR: str = "x" โ†’ AnnAssign โ† ast.Assign ๋งŒ ๊ฒ€์‚ฌํ•˜๋ฉด ์™„์ „ํžˆ ๋†“์นจ +OTHER = 1 โ†’ Assign +``` + +Rev.1 ์˜ AST ๊ฐ€๋“œ๋Š” `ast.Assign` ๋งŒ ์ˆœํšŒํ•˜๋ฏ€๋กœ **ํƒ€์ž… ์ฃผ์„์ด ๋ถ™์€ ์ „์—ญ ์žฌ๋„์ž…์„ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค.** ์ง€์  ๊ทธ๋Œ€๋กœ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค. + +### 2.4 ํ™•์ธ๋œ ๋น„์ด์Šˆ โ€” `__all__` / `import *` + +`__dir__` ๋„์ž… ์‹œ `from mqtt_common import *` ํ‘œ๋ฉด์ด ๊ฑฑ์ •๋  ์ˆ˜ ์žˆ์œผ๋‚˜: + +- `mqtt_common.py` ์— **`__all__` ์ •์˜ 0๊ฑด** +- ์ €์žฅ์†Œ ์ „์ฒด์— **`from mqtt_common import *` 0๊ฑด** + +`import *` ๋Š” `__all__` ์ด ์—†์œผ๋ฉด ๋ชจ๋“ˆ ์ „์—ญ์„ ์—ด๊ฑฐํ•˜๋ฉฐ `__dir__` ์„ ์“ฐ์ง€ ์•Š์œผ๋ฏ€๋กœ, ์–ด๋А ์ชฝ์œผ๋กœ๋„ ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค. (`LOGS_DIR` ์ด `import *` ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€์ง€ ์•Š๋Š” ๊ฒƒ์€ Rev.1 ยง4 ์˜ from-import ๋ถ„์„๊ณผ ๊ฐ™์€ ๊ฒฐ๋ก ์ž…๋‹ˆ๋‹ค.) + +--- + +## 3. ๐Ÿ†• Rev.2 ์ž์ฒด ๋ฐœ๊ฒฌ โ€” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ€๋“œ์˜ ์•ฝํ•œ ๋‹จ์–ธ + +Rev.1 ยง5.1 ์„ธ ๋ฒˆ์งธ ๊ฐ€๋“œ์˜ ๋งˆ์ง€๋ง‰ ์ค„: + +```python +monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR") +assert "/tmp/b9-override" != mq.get_logs_dir() # โ† ๋ถ€๋“ฑํ˜ธ ๋‹จ์–ธ +``` + +๋ถ€๋“ฑํ˜ธ๋Š” **๊ฑฐ์˜ ๋ชจ๋“  ์˜ค๋™์ž‘์„ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค.** `get_logs_dir()` ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋‚˜ `None`, ์—‰๋šฑํ•œ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด๋„ `"/tmp/b9-override"` ์™€ ๋‹ค๋ฅด๊ธฐ๋งŒ ํ•˜๋ฉด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๊ฒ€์ฆํ•ด์•ผ ํ•  ๊ฒƒ์€ "ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ง€์šฐ๋ฉด **cwd ๊ธฐ๋ฐ˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋Œ์•„์˜จ๋‹ค**"์ž…๋‹ˆ๋‹ค. Rev.2 ์—์„œ ๋“ฑํ˜ธ ๋‹จ์–ธ์œผ๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค(ยง5.1). + +--- + +## 4. ์„ค๊ณ„ (Rev.1 ์œ ์ง€ + `__dir__` ์ถ”๊ฐ€) + +```python +def get_logs_dir() -> str: + """Audit-log root, resolved at call time (B-9). + + Overridable with ``DELEGATE_JOB_LOGS_DIR``; otherwise + ``/.mam/delegate_job_logs``. Resolved per call rather than at import + so a chdir after import cannot strand the audit trail in the old tree โ€” + the same reason ``DEFAULT_REGISTRY_DIR`` stays a relative string. + """ + env = os.environ.get("DELEGATE_JOB_LOGS_DIR") + if env and env.strip(): + return env + return os.path.join(os.getcwd(), ".mam", "delegate_job_logs") + + +def __getattr__(name: str): # PEP 562 (3.7+) + """Keep ``mqtt_common.LOGS_DIR`` working for external consumers + (documented in registry.md) while resolving it dynamically.""" + if name == "LOGS_DIR": + return get_logs_dir() + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): # PEP 562 ๊ถŒ์žฅ โ€” dir()/ํƒญ ์™„์„ฑ ์œ ์ง€ + return sorted(set(globals()) | {"LOGS_DIR"}) +``` + +`_default_logs_dir` โ†’ `get_logs_dir` ๊ฐœ๋ช…, ๋ชจ๋“ˆ ์ „์—ญ `LOGS_DIR = โ€ฆ` ๋Œ€์ž… **์‚ญ์ œ**. + +### 4.1 ๊ตฌํ˜„ ํ•จ์ • 3์ข… (Rev.1 ยง2 ์œ ์ง€ โ€” ์ „๋ถ€ ์‹ค์ธก) + +| # | ํ•จ์ • | ์‹ค์ธก | +|---|---|---| +| **T1** | ์ „์—ญ์„ ๋‚จ๊ธฐ๋ฉด `__getattr__` ์ด **ํ˜ธ์ถœ์กฐ์ฐจ ์•ˆ ๋จ** | ์ˆ˜์ • ํ›„์—๋„ chdir ์‹œ stale | +| **T2** | ๋ชจ๋“ˆ **๋‚ด๋ถ€** ๋งจ์ด๋ฆ„ `LOGS_DIR` ์€ `__getattr__` ๋Œ€์ƒ ์•„๋‹˜ | `NameError` | +| **T3** | ๊ทธ `NameError` ๋ฅผ best-effort `except Exception` ์ด **์‚ผํ‚ด** | `logger.warning` ๋งŒ ๋‚จ๊ณ  ์ •์ƒ ๋ฐ˜ํ™˜ โ†’ ๋ฌด์Œ ๋กœ๊ทธ ์†Œ์‹ค + ์ „ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | + +T3 ๋•Œ๋ฌธ์— ๊ฐ€๋“œ ํ•˜๋‚˜๋Š” **๋ฐ˜๋“œ์‹œ ์‹ค์ œ ํŒŒ์ผ ์ƒ์„ฑ**์„ ๋‹จ์–ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 5. ๊ตฌํ˜„ ๊ณ„ํš + +### 5.1 ๋‹จ๊ณ„ 1 โ€” `mqtt_common.py` + +1. `_default_logs_dir()` โ†’ `get_logs_dir()` ๊ฐœ๋ช… + docstring +2. **`LOGS_DIR = _default_logs_dir()` ์‚ญ์ œ** (T1) +3. `__getattr__` ์ถ”๊ฐ€ +4. **`__dir__` ์ถ”๊ฐ€ (์ง‘ํ•ฉ ๊ธฐ๋ฐ˜)** โ† C2-a +5. `:431` `Path(logs_dir or LOGS_DIR)` โ†’ `Path(logs_dir or get_logs_dir())` (T2) +6. `:579` ๋™์ผ ๊ต์ฒด (T2) + +### 5.2 ๋‹จ๊ณ„ 2 โ€” `registry.py` + +`:198`ยท`:389` ์˜ `mqtt_common.LOGS_DIR` โ†’ `mqtt_common.get_logs_dir()`. + +### 5.3 ๋‹จ๊ณ„ 3 โ€” `registry.md` + +`:168` ํ—ฌํผ ๋ชฉ๋ก์— `get_logs_dir` ์ถ”๊ฐ€, `LOGS_DIR` ์ด ๋™์  ํ˜ธํ™˜ ๋ณ„์นญ์ž„์„ 1์ค„ ๋ช…์‹œ. `BOOTSTRAP*.md` ๋Š” ๋™์ž‘ ๋ฌด๋ณ€๊ฒฝ์ด๋ฏ€๋กœ ์†๋Œ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +--- + +## 6. ํšŒ๊ท€ ๊ฐ€๋“œ (ํ™•์ •) + +### 6.1 `tests/test_tier1_unit.py` ์— ์ถ”๊ฐ€ + +```python +def test_b9_logs_dir_follows_cwd_changes(mam_sandbox, tmp_path, monkeypatch): + """B-9: the audit-log root must be resolved per call, not frozen at import.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR", raising=False) + a = tmp_path / "a"; b = tmp_path / "b" + a.mkdir(); b.mkdir() + + # realpath on both sides: pytest's tmp_path happens to be pre-resolved today, + # but relying on that is an undocumented dependency (C1'). + def logs_under(p): + return os.path.realpath(os.path.join(str(p), ".mam", "delegate_job_logs")) + + monkeypatch.chdir(a) + assert os.path.realpath(mq.get_logs_dir()) == logs_under(a) + monkeypatch.chdir(b) + assert os.path.realpath(mq.get_logs_dir()) == logs_under(b) + # the compat alias must follow too (T1: a surviving global fails here) + assert os.path.realpath(mq.LOGS_DIR) == logs_under(b) + + +def test_b9_audit_log_lands_under_the_current_cwd(mam_sandbox, tmp_path, monkeypatch): + """B-9/T3: assert the FILE appears โ€” a swallowed NameError must not pass.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR", raising=False) + monkeypatch.chdir(tmp_path) + mq.init_job_log("b9job", {"status": "pending"}) + assert (tmp_path / ".mam" / "delegate_job_logs" / "b9job" / "meta.json").exists(), \ + "audit log did not land under the current cwd (the best-effort handler may have swallowed an error)" + + +def test_b9_logs_dir_env_override_is_dynamic(mam_sandbox, tmp_path, monkeypatch): + """B-9: DELEGATE_JOB_LOGS_DIR must be honoured at call time, both ways.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DELEGATE_JOB_LOGS_DIR", "/tmp/b9-override") + assert mq.get_logs_dir() == "/tmp/b9-override" + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR") + # equality, not inequality โ€” clearing the env must restore the cwd default (Rev.2 ยง3) + assert os.path.realpath(mq.get_logs_dir()) == \ + os.path.realpath(os.path.join(str(tmp_path), ".mam", "delegate_job_logs")) + + +def test_b9_no_module_level_logs_dir_binding(): + """B-9/T1: a surviving module global would make __getattr__ dead code.""" + import ast, pathlib + src = (pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills" + / "multi-agent-mux-delegate-job" / "scripts" / "mqtt_common.py") + tree = ast.parse(src.read_text()) + for node in tree.body: # module scope only + if isinstance(node, ast.Assign): + for t in node.targets: + assert not (isinstance(t, ast.Name) and t.id == "LOGS_DIR"), \ + f"line {node.lineno}: module-level LOGS_DIR binding shadows __getattr__ (B-9/T1)" + elif isinstance(node, ast.AnnAssign): # C2-b: LOGS_DIR: str = ... parses as AnnAssign + assert not (isinstance(node.target, ast.Name) and node.target.id == "LOGS_DIR"), \ + f"line {node.lineno}: annotated module-level LOGS_DIR binding shadows __getattr__ (B-9/T1)" + + +def test_b9_logs_dir_stays_discoverable(mam_sandbox): + """B-9/C2-a: PEP 562 __dir__ keeps LOGS_DIR visible to dir() and tooling.""" + mq = get_mqtt_common(mam_sandbox) + assert "LOGS_DIR" in dir(mq) + assert hasattr(mq, "LOGS_DIR") # true via __getattr__ even without __dir__ + assert dir(mq).count("LOGS_DIR") == 1 # set-based __dir__ must not duplicate +``` + +**Rev.1 ๋Œ€๋น„ ๋ณ€๊ฒฝ** + +| # | ๋ณ€๊ฒฝ | ๊ทผ๊ฑฐ | +|---|---|---| +| 1 | ๊ฐ€๋“œ 1 ์„ `os.path.realpath` ์–‘๋ณ€ ์ •๊ทœํ™”๋กœ ๊ต์ฒด | C1' | +| 2 | ๊ฐ€๋“œ 3 ์˜ ๋งˆ์ง€๋ง‰ ๋‹จ์–ธ์„ ๋ถ€๋“ฑํ˜ธ โ†’ **๋“ฑํ˜ธ** | Rev.2 ยง3 | +| 3 | ๊ฐ€๋“œ 4 ์— `ast.AnnAssign` ๋ถ„๊ธฐ ์ถ”๊ฐ€ | C2-b | +| 4 | **๊ฐ€๋“œ 5 ์‹ ์„ค** (`dir()` ๊ฐ€์‹œ์„ฑ + ์ค‘๋ณต ์—†์Œ) | C2-a, ยง2.2 | + +๊ฐ€๋“œ๋Š” 4์ข… โ†’ **5์ข…**์ž…๋‹ˆ๋‹ค. + +### 6.2 ๋ฎคํ…Œ์ด์…˜ ๊ฒ€์ฆ (๊ตฌํ˜„์ž ํ•„์ˆ˜) + +| # | ๋ฎคํ…Œ์ด์…˜ | ๊ธฐ๋Œ€ | +|---|---|---| +| **M1** | `LOGS_DIR = get_logs_dir()` ์ „์—ญ ๋˜์‚ด๋ฆผ (T1) | ๊ฐ€๋“œ 1ยท4 **FAIL** | +| **M1b** | `LOGS_DIR: str = get_logs_dir()` ๋กœ ๋˜์‚ด๋ฆผ (C2-b) | ๊ฐ€๋“œ 4 **FAIL** โ† Rev.1 ๊ฐ€๋“œ๋กœ๋Š” ํ†ต๊ณผํ–ˆ์„ ์ผ€์ด์Šค | +| **M2** | `:431` ์„ ๋งจ์ด๋ฆ„ `LOGS_DIR` ๋กœ ๋˜๋Œ๋ฆผ (T2ยทT3) | ๊ฐ€๋“œ 2 **FAIL** (๊ฐ€๋“œ 1ยท3 ์€ ํ†ต๊ณผ โ€” T3 ๋ฌด์Œ์„ฑ ์ฆ๋ช…) | +| **M3** | `get_logs_dir()` ๋‚ด๋ถ€๋ฅผ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹œ ๊ณ„์‚ฐ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | ๊ฐ€๋“œ 1 **FAIL** | +| **M4** | `__getattr__` ์‚ญ์ œ | ๊ฐ€๋“œ 1ยท5 **FAIL** | +| **M5** | `__dir__` ์‚ญ์ œ | ๊ฐ€๋“œ 5 **FAIL** (`hasattr` ์€ ์—ฌ์ „ํžˆ ํ†ต๊ณผ โ€” ยง2.1 ์˜ ๊ตฌ๋ถ„์„ ์ฆ๋ช…) | + +**M2 ๊ฐ€ ์—ฌ์ „ํžˆ ํ•ต์‹ฌ**์ž…๋‹ˆ๋‹ค. **M1bยทM5 ๋Š” ์ด๋ฒˆ ๋ผ์šด๋“œ์—์„œ ์ถ”๊ฐ€**๋œ ๊ฒƒ์œผ๋กœ ๊ฐ๊ฐ C2-bยทC2-a ์— ๋Œ€์‘ํ•ฉ๋‹ˆ๋‹ค. ์ „๋ถ€ ๊ธฐ๋Œ€๋Œ€๋กœ FAIL ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ€๋“œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. + +--- + +## 7. ๊ฒ€์ฆ ์ ˆ์ฐจ + +| # | ํ™•์ธ | ๊ธฐ๋Œ€ | +|---|---|---| +| 1 | `python -c "import mqtt_common"` | OK | +| 2 | import โ†’ `chdir` โ†’ `mqtt_common.LOGS_DIR` | ์ƒˆ cwd ๋ฐ˜์˜ | +| 3 | `chdir` ํ›„ `init_job_log` โ†’ ํŒŒ์ผ ์œ„์น˜ | **์ƒˆ cwd ์•„๋ž˜ ์ƒ์„ฑ** (T3 โ€” ํ•ต์‹ฌ) | +| 4 | `grep -n "^LOGS_DIR" mqtt_common.py` | 0๊ฑด (T1) | +| 5 | `grep -n "or LOGS_DIR" mqtt_common.py` | 0๊ฑด (T2) | +| 6 | `python -c "import mqtt_common as m; print('LOGS_DIR' in dir(m), dir(m).count('LOGS_DIR'))"` | `True 1` (C2-a) | +| 7 | `DELEGATE_JOB_LOGS_DIR` ์„ค์ •/ํ•ด์ œ | ์ฆ‰์‹œ ๋ฐ˜์˜, ํ•ด์ œ ์‹œ cwd ๊ธฐ๋ณธ๊ฐ’ ๋ณต๊ท€ | +| 8 | `registry.py logs --list` | ํšŒ๊ท€ ์—†์Œ | +| 9 | **๋ฎคํ…Œ์ด์…˜ M1ยทM1bยทM2ยทM3ยทM4ยทM5** | ๊ฐ๊ฐ ๊ธฐ๋Œ€๋Œ€๋กœ FAIL | +| 10 | `pytest tests/ -q` | **276 passed** (271 ์‹ค์ธก + ๊ฐ€๋“œ 5๊ฑด) | +| 11 | `env -u PYTHONPATH pytest tests/test_tier1_unit.py -q` | ํ†ต๊ณผ (ํ™˜๊ฒฝ ๋น„์˜์กด) | +| 12 | `IMPROVEMENTS.md` `:5` โ†” `:70` / `:6` โ†” `:85` ๋Œ€์กฐ | ๊ฐ๊ฐ ์ผ์น˜ | + +10๋ฒˆ์€ ์•ฝ 7๋ถ„ ์†Œ์š”๋ฉ๋‹ˆ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰ ๊ถŒ์žฅ. + +--- + +## 8. ๋ฌธ์„œ ๋™๊ธฐํ™” + +### 8.1 `IMPROVEMENTS.md` โ€” 7๊ณณ + +| ํ–‰ | ํ˜„์žฌ | ๋ณ€๊ฒฝ ํ›„ | +|---|---|---| +| `:3` | ์ตœ์ข… ๊ฐฑ์‹ ์ผ (โ€ฆ 271/271) | B-9 ์™„๋ฃŒ ๋ฐ **276/276** ๋ฐ˜์˜ | +| `:5` | ๋ฏธํ•ด๊ฒฐ **2๊ฑด** (์•„ํ‚ค 1, **์—ฃ์ง€ 1**) | ๋ฏธํ•ด๊ฒฐ **1๊ฑด** (์•„ํ‚ค 1, **์—ฃ์ง€ 0**) | +| `:6` | ์™„๋ฃŒ **23๊ฑด** | ์™„๋ฃŒ **24๊ฑด**, ๋ชฉ๋ก์— `B-9` ์ถ”๊ฐ€ | +| `:70` | `## 2. โ€ฆ (Edge-case Bugs โ€” 1๊ฑด)` | `โ€ฆ (Edge-case Bugs โ€” 0๊ฑด โ€” ์ „์› ์™„๋ฃŒ)` | +| `:72-73` | B-9 ํ•ญ๋ชฉ | **์‚ญ์ œ** (ยง5 ๋กœ ์ด๋™) | +| `:85` | `## 5. โ€ฆ (Completed Tasks โ€” 23๊ฑด)` | `โ€ฆ (Completed Tasks โ€” 24๊ฑด)` | +| `:251` | ๋กœ๋“œ๋งต P4-1 ํ–‰ | `โ€ฆ **(โœ… ์™„๋ฃŒ โ€” ์ „์ฒด 276/276 PASS)**` | + +ยง5 ์‹ ๊ทœ ํ•ญ๋ชฉ โ€” Rev.1 ๋ฌธ์•ˆ์— ๋‹ค์Œ ํ•œ ์ค„์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + +```markdown +- PEP 562 `__dir__` ์„ ํ•จ๊ป˜ ์ •์˜ํ•ด `dir(mqtt_common)` ๋ฐ ํƒญ ์™„์„ฑ์—์„œ `LOGS_DIR` ์ด ๊ณ„์† + ๋ณด์ด๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค(`hasattr` ์€ `__getattr__` ๋งŒ์œผ๋กœ๋„ ๋™์ž‘ํ•˜๋ฏ€๋กœ ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค). +``` + +**์ฃผ์˜**: `:5` ์—ฃ์ง€ ์นด์šดํŠธ์™€ `:70` ยง2 ํ—ค๋”๋Š” **๋ฐ˜๋“œ์‹œ ํ•จ๊ป˜** ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. + +### 8.2 `VERSIONS.md` + +`#### 9` ์‹ ์„ค. Rev.1 ๋ฌธ์•ˆ์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + +```markdown +- PEP 562 `__dir__` ๋ณ‘ํ–‰ ์ •์˜๋กœ `dir()`ยทํƒญ ์™„์„ฑ ๊ฐ€์‹œ์„ฑ ์œ ์ง€. +``` + +์ „์ฒด ํšŒ๊ท€ ์ˆ˜์น˜๋Š” **276** ์œผ๋กœ ๊ธฐ์žฌํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 9. ๊ทœ๋ชจ ๋ฐ ๋ฆฌ์Šคํฌ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ | +|---|---| +| `mqtt_common.py` | ํ•จ์ˆ˜ ๊ฐœ๋ช… + docstring, ์ „์—ญ ์‚ญ์ œ, `__getattr__`ยท`__dir__` ์ถ”๊ฐ€(~9์ค„), ์†Œ๋น„์ž 2๊ณณ | +| `registry.py` | 2๊ณณ | +| `registry.md` | 1~3์ค„ | +| `tests/test_tier1_unit.py` | ๊ฐ€๋“œ **5๊ฑด** | +| `IMPROVEMENTS.md` / `VERSIONS.md` | ์นด์šดํŠธยทํ•ญ๋ชฉ ์ด๋™ + changelog | +| **ํ…Œ์ŠคํŠธ ์ด๊ณ„** | 271 (์‹ค์ธก) โ†’ **276** | + +| ๋ฆฌ์Šคํฌ | ํ‰๊ฐ€ | +|---|---| +| **T1 โ€” ์ „์—ญ ์ž”์กด์œผ๋กœ ์ˆ˜์ • ๋ฌดํšจ** | ๐Ÿ”ด ๊ฐ€๋“œ 1ยท4 + M1ยท**M1b**. AST ๊ฐ€๋“œ๊ฐ€ ์ฃผ์„ ๋Œ€์ž…๊นŒ์ง€ ๋ฎ์Œ | +| **T3 โ€” ๋ฌด์Œ ๋กœ๊ทธ ์†Œ์‹ค** | ๐Ÿ”ด ๊ฐ€๋“œ 2(ํŒŒ์ผ ์กด์žฌ) + M2. ๋ฌธ์ž์—ด ๋‹จ์–ธ๋งŒ์œผ๋กœ๋Š” ๋ชป ์žก์Œ | +| ๊ฐ€๋“œ 1 ์˜ ํ”Œ๋žซํผ ์˜์กด | `realpath` ์ •๊ทœํ™”๋กœ ์ œ๊ฑฐ (C1') | +| `__dir__` ๋„์ž… ๋ถ€์ž‘์šฉ | `__all__` ์—†์Œยท`import *` 0๊ฑด ํ™•์ธ โ†’ ์˜ํ–ฅ ์—†์Œ | +| ์„ฑ๋Šฅ | `get_logs_dir()` ๋Š” `getcwd` 1ํšŒ + `join`. ์ด๋ฏธ ํŒŒ์ผ I/O ํ•˜๋Š” ๊ฒฝ๋กœ โ€” ๋ฌด์‹œ ๊ฐ€๋Šฅ | +| ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋™์  ๋ฐ˜์˜ | ์˜๋„๋œ ๊ฐœ์„ . ์™„๋ฃŒ ๋…ธํŠธ์— ๋ช…์‹œ | +| ํ…Œ์ŠคํŠธ ๊ฐ„ cwd ๋ˆ„์ˆ˜ | ๊ฐ€๋“œ๋Š” `monkeypatch.chdir` ๋งŒ ์‚ฌ์šฉ | + +### ๊ถŒ์žฅ ์ปค๋ฐ‹ ๋ถ„ํ•  + +1. `fix(mqtt): resolve the audit-log root per call instead of at import (B-9)` โ€” ยง5.1~5.2 +2. `test(b9): guard cwd-following, real file placement, env round-trip, global re-binding, and discoverability` โ€” ยง6 +3. `docs: sync registry.md, IMPROVEMENTS.md and VERSIONS.md for B-9` โ€” ยง5.3 + ยง8 + +--- + +## 10. ํ•œ๊ณ„ + +- ๋ณธ ๊ณ„ํš์€ Planner ์‚ฐ์ถœ๋ฌผ์ด๋ฉฐ **์ €์žฅ์†Œ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค**(์ž‘์—… ํŠธ๋ฆฌ ๊ณ„ํš ์ „ํ›„ clean). ํ”„๋กœํ† ํƒ€์ž…์€ `$TMPDIR` ์—์„œ ์ˆ˜ํ–‰ ํ›„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค. +- **์ด๋ฒˆ ๋ผ์šด๋“œ์— ์‹ค์ธกํ•œ ๊ฒƒ**: pytest `tmp_path` ๊ฐ€ ์ด๋ฏธ resolve ๋œ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜(C1 ๊ฒฐ๋ก  ๊ธฐ๊ฐ), `tempfile.gettempdir()` ๋Š” ๋ฏธํ•ด๊ฒฐ(์ฑŒ๋ฆฐ์ € ์žฌํ˜„ ์ž์ฒด๋Š” ์œ ํšจ), ํŒŒ์ผ ์กด์žฌ ๊ฐ€๋“œ๋Š” ์‹ฌ๋งํฌ์™€ ๋ฌด๊ด€, `__dir__` ์œ ๋ฌด์— ๋”ฐ๋ฅธ `dir()` ์ฐจ์ด์™€ **`hasattr` ์€ ๋ฌด๊ด€**ํ•จ, ์ฑŒ๋ฆฐ์ € `__dir__` ์•ˆ์˜ ์ค‘๋ณต(count=2), `AnnAssign` ์ด `ast.Assign` ๊ฒ€์‚ฌ๋ฅผ ์šฐํšŒํ•จ, `__all__`ยท`import *` ๋ถ€์žฌ. +- **์—ฌ์ „ํžˆ ์‹คํ–‰ ๊ฒ€์ฆํ•˜์ง€ ์•Š์€ ๊ฒƒ**: ยง6 ์˜ ๊ฐ€๋“œ ์ฝ”๋“œ๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ยง4 ์„ค๊ณ„๋Š” ์ถ•์†Œ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ๋งŒ ํ™•์ธํ–ˆ๊ณ  ์‹ค์ œ `mqtt_common.py` ์— ์ ์šฉํ•ด ๋ณด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ยง6.2 ๋ฎคํ…Œ์ด์…˜์ด ๊ทธ ๋Œ€์ฒด ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. +- pytest `tmp_path` ์˜ ์‚ฌ์ „ resolve ๋Š” **๊ด€์ธก๋œ ๋™์ž‘**์ด๋ฉฐ ๋ฌธ์„œํ™”๋œ ๊ณ„์•ฝ์€ ์•„๋‹™๋‹ˆ๋‹ค โ€” ๊ทธ๋ž˜์„œ ์ •๊ทœํ™”๋ฅผ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. +- ๋ฒ ์ด์Šค๋ผ์ธ์€ `8cee937` ์—์„œ **271 passed in 430.64s** ์‹ค์ธก(`IMPROVEMENTS.md:3` ๊ณผ ์ผ์น˜). ยง7-10 ์˜ 276 ์€ ๊ฐ€๋“œ 5๊ฑด์„ ๋”ํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. +- `registry.md` ์™ธ ์™ธ๋ถ€ ๋ฌธ์„œ์˜ `LOGS_DIR` ์ฐธ์กฐ๋Š” ์ €์žฅ์†Œ ๋‚ด๋ถ€๋งŒ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-c35385ad.md b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-c35385ad.md new file mode 100644 index 0000000..5451c23 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-c35385ad.md @@ -0,0 +1,164 @@ +# Cross-Code Review Report: B-9 (LOGS_DIR import-time cwd freeze fix) + +- **Job ID**: c35385ad +- **Reviewer**: cline +- **Date**: 2026-08-17 +- **Backlog Item**: B-9 (P4-1) โ€” `LOGS_DIR` import-time cwd freeze resolution +- **Changed Files**: `mqtt_common.py`, `registry.py`, `registry.md`, `tests/test_tier1_unit.py`, `IMPROVEMENTS.md`, `VERSIONS.md` + +--- + +## 1. Objective + +Verify that the B-9 implementation correctly refactors `mqtt_common.py` and `registry.py` to resolve the audit-log root (`LOGS_DIR`) dynamically at call time via `get_logs_dir()`, eliminating the import-time `os.getcwd()` freeze that caused audit-log path drift after `chdir`. Backward compatibility for `mqtt_common.LOGS_DIR` consumers must be preserved, 5 dedicated regression tests must be added, and documentation must be accurate. + +--- + +## 2. Implementation Review + +### 2.1 `mqtt_common.py` โ€” Core Fix + +**Before:** +```python +def _default_logs_dir() -> str: ... +LOGS_DIR = _default_logs_dir() # frozen at import time +``` + +**After:** +```python +def get_logs_dir() -> str: + """Audit-log root, resolved at call time (B-9). ...""" + env = os.environ.get("DELEGATE_JOB_LOGS_DIR") + if env and env.strip(): + return env + return os.path.join(os.getcwd(), ".mam", "delegate_job_logs") + +def __getattr__(name: str): # PEP 562 (3.7+) + if name == "LOGS_DIR": + return get_logs_dir() + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + +def __dir__(): + return sorted(set(globals()) | {"LOGS_DIR"}) +``` + +**Assessment:** +- The module-level `LOGS_DIR = _default_logs_dir()` assignment is **removed** โ€” verified by AST guard test and manual grep (0 hits at module scope). +- `get_logs_dir()` is now **public** (renamed from `_default_logs_dir`), resolving the path per call. +- PEP 562 `__getattr__` provides backward-compatible `mqtt_common.LOGS_DIR` access, resolving dynamically each time. +- PEP 562 `__dir__` keeps `LOGS_DIR` discoverable in `dir()` and tab-completion. +- `__getattr__` correctly raises `AttributeError` for unknown attributes (prevents infinite recursion in `hasattr`). + +**Internal callers updated (all resolve through `get_logs_dir()` when `logs_dir=None`):** +| Function | Line | Pattern | +|---|---|---| +| `job_log_dir` | 441 | `Path(logs_dir or get_logs_dir()) / job_id` | +| `job_log_path` | 444 | delegates to `job_log_dir` | +| `append_event` | 488 | delegates to `job_log_path` | +| `init_job_log` | 523 | delegates to `job_log_dir` | +| `update_logged_status` | 505 | delegates to `job_log_path` | +| `read_logged_meta` | 553 | delegates to `job_log_path` | +| `read_logged_status` | 561 | delegates to `job_log_path` | +| `iter_logged_events` | 572 | delegates to `job_log_path` | +| `list_logged_jobs` | 589 | `Path(logs_dir or get_logs_dir())` | + +All 9 audit-log functions chain through `get_logs_dir()` when no explicit `logs_dir` is passed. **No stale `or LOGS_DIR` (bare global) references remain.** + +### 2.2 `registry.py` โ€” Consumer Updates + +Two references updated from `mqtt_common.LOGS_DIR` to `mqtt_common.get_logs_dir()`: +- Line 198 (`get_feedback`): `logs_dir = mqtt_common.get_logs_dir()` โœ“ +- Line 389 (`_cmd_logs`): `logs_dir = args.logs_dir or mqtt_common.get_logs_dir()` โœ“ + +### 2.3 `registry.md` โ€” Documentation + +Helper list updated to describe `get_logs_dir` as the primary API with `LOGS_DIR` noted as a "dynamic compat alias". Accurate and consistent with the implementation. + +### 2.4 Backward Compatibility + +- `from mqtt_common import LOGS_DIR` โ€” **0 occurrences** in the entire repository (verified by grep). Compat surface 100% covered by `__getattr__`. +- `mqtt_common.LOGS_DIR` attribute access โ€” preserved via PEP 562 `__getattr__`, resolves dynamically. +- `DELEGATE_JOB_LOGS_DIR` env override โ€” now reflected at call time (bonus improvement, not a regression). + +### 2.5 Test Suite (`tests/test_tier1_unit.py`) + +5 new B-9 regression tests (lines 406โ€“473): + +| Test | Guard Type | What It Verifies | +|---|---|---| +| `test_b9_logs_dir_follows_cwd_changes` | T1 (dynamic) | `get_logs_dir()` and `LOGS_DIR` compat alias follow `chdir` | +| `test_b9_audit_log_lands_under_the_current_cwd` | T3 (file creation) | Actual `meta.json` file appears under current cwd (catches swallowed errors) | +| `test_b9_logs_dir_env_override_is_dynamic` | Env dynamic | `DELEGATE_JOB_LOGS_DIR` honored at call time; clearing restores cwd default | +| `test_b9_no_module_level_logs_dir_binding` | T1 (AST static) | No module-level `LOGS_DIR` assignment (covers `Assign` and `AnnAssign`) | +| `test_b9_logs_dir_stays_discoverable` | C2-a (PEP 562) | `LOGS_DIR` in `dir()`, `hasattr` works, no duplicates | + +The T3 guard is particularly well-designed โ€” it asserts the **actual file** appears on disk, not just string equality. This is critical because the audit-log layer uses best-effort `except Exception` that swallows errors silently. + +### 2.6 Documentation (`IMPROVEMENTS.md`, `VERSIONS.md`) + +**IMPROVEMENTS.md:** Header updated (date, 276/276, 24 completed, 1 open). B-9 moved to completed section (lines 84โ€“90). Edge-case section shows "0๊ฑด โ€” ์ „์› ์™„๋ฃŒ". โœ“ + +--- + +## 3. Verification Results + +### 3.1 Syntax Checks (`py_compile`) +| File | Result | +|---|---| +| `mqtt_common.py` | โœ… PASS | +| `registry.py` | โœ… PASS | +| `tests/test_tier1_unit.py` | โœ… PASS | + +### 3.2 Stale Reference Scan +| Check | Result | +|---|---| +| `from mqtt_common import LOGS_DIR` in source | โœ… 0 occurrences | +| Bare `or LOGS_DIR` (global) in source | โœ… 0 occurrences | +| Module-level `LOGS_DIR =` assignment | โœ… 0 occurrences (removed) | + +### 3.3 B-9 Targeted Tests +``` +tests/test_tier1_unit.py::test_b9_logs_dir_follows_cwd_changes PASSED [ 20%] +tests/test_tier1_unit.py::test_b9_audit_log_lands_under_the_current_cwd PASSED [ 40%] +tests/test_tier1_unit.py::test_b9_logs_dir_env_override_is_dynamic PASSED [ 60%] +tests/test_tier1_unit.py::test_b9_no_module_level_logs_dir_binding PASSED [ 80%] +tests/test_tier1_unit.py::test_b9_logs_dir_stays_discoverable PASSED [100%] +5 passed, 30 deselected in 0.10s +``` + +### 3.4 Full Test Suite +``` +276 passed in 422.40s (0:07:02) +``` +**Zero failures, zero errors, zero regressions.** Test count increased from 271 โ†’ 276 (+5 new B-9 tests), consistent with documentation claims. + +--- + +## 4. Lint & Quality Assessment + +- **No unused imports** introduced by the change. +- **No dead code** โ€” `__getattr__` and `__dir__` are both exercised by tests. +- **PEP 562** is the idiomatic Python โ‰ฅ3.7 pattern for dynamic module attributes; test environment runs Python 3.9.6. +- **Thread safety**: `get_logs_dir()` calls `os.environ.get()` and `os.getcwd()`, both thread-safe in CPython. Per-call overhead is negligible vs. the file I/O it precedes. +- **No surgical-change violations**: every changed line traces directly to the B-9 requirement. + +--- + +## 5. Concerns & Observations + +1. **Minor (non-blocking):** `__dir__` returns `sorted(set(globals()) | {"LOGS_DIR"})` โ€” `LOGS_DIR` would still appear in `dir()` even if `__getattr__` were removed. Purely cosmetic; the AST guard test catches actual binding regressions. +2. **No escalation needed:** The fix is a clean, surgical refactor. No design-level rework required. + +--- + +## 6. Verdict + +The B-9 implementation is **correct, complete, and well-tested**: +- The import-time cwd freeze is eliminated โ€” `get_logs_dir()` resolves per call. +- Backward compatibility is fully preserved via PEP 562 `__getattr__`/`__dir__`. +- All internal callers and external consumers (`registry.py`) are updated. +- 5 high-quality regression tests guard against regression (AST static guard + file-creation guard). +- Documentation (`IMPROVEMENTS.md`, `VERSIONS.md`, `registry.md`) is accurate. +- Full test suite: **276/276 PASS**, zero regressions. + +[VERDICT: PASS] \ No newline at end of file diff --git a/.agents/skills/multi-agent-mux-delegate-job/registry.md b/.agents/skills/multi-agent-mux-delegate-job/registry.md index 0e8a5b9..c69d0b9 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/registry.md +++ b/.agents/skills/multi-agent-mux-delegate-job/registry.md @@ -165,7 +165,9 @@ even after the registry dir is cleaned up. It is git-ignored. | event received | `received` | `job_subscriber.py` | Helpers live in [`./scripts/mqtt_common.py`](./scripts/mqtt_common.py): -`LOGS_DIR`, `job_log_path`, `init_job_log`, `append_event` (fcntl-locked, +`get_logs_dir` (audit-log root, resolved per call โ€” a chdir after import no +longer strands the trail; `LOGS_DIR` remains as a dynamic compat alias), +`job_log_path`, `init_job_log`, `append_event` (fcntl-locked, concurrent-append safe), `update_logged_status`, and the readers `read_logged_meta` / `read_logged_status` / `iter_logged_events` / `list_logged_jobs`. Every writer is **best-effort and isolated** โ€” wrapped in diff --git a/.agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py b/.agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py index ac442be..a441e36 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py +++ b/.agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py @@ -128,20 +128,30 @@ EVENTS_FILENAME = "events.ndjson" STATUS_FILENAME = "status.json" -def _default_logs_dir() -> str: - """Audit-log root. Overridable with ``DELEGATE_JOB_LOGS_DIR``; otherwise - ``/.mam/delegate_job_logs`` โ€” we keep audit logs next to the - live registry (``.mam/jobs/``) so the two runtime artifacts sit - under the same parent dir and follow the same ``.gitignore`` rule. - The cwd of whichever process emits events (the bash wrapper and - scripts) is used as the anchor.""" +def get_logs_dir() -> str: + """Audit-log root, resolved at call time (B-9). + + Overridable with ``DELEGATE_JOB_LOGS_DIR``; otherwise + ``/.mam/delegate_job_logs``. Resolved per call rather than at import + so a chdir after import cannot strand the audit trail in the old tree โ€” + the same reason ``DEFAULT_REGISTRY_DIR`` stays a relative string. + """ env = os.environ.get("DELEGATE_JOB_LOGS_DIR") if env and env.strip(): return env return os.path.join(os.getcwd(), ".mam", "delegate_job_logs") -LOGS_DIR = _default_logs_dir() +def __getattr__(name: str): # PEP 562 (3.7+) + """Keep ``mqtt_common.LOGS_DIR`` working for external consumers + (documented in registry.md) while resolving it dynamically.""" + if name == "LOGS_DIR": + return get_logs_dir() + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): # PEP 562 recommendation โ€” keep dir()/tab-completion discoverable + return sorted(set(globals()) | {"LOGS_DIR"}) # -------------------------------------------------------------------------- @@ -428,7 +438,7 @@ def _utcnow_precise() -> str: # touched (it is reserved for data output). # -------------------------------------------------------------------------- def job_log_dir(job_id: str, logs_dir: Optional[str] = None) -> Path: - return Path(logs_dir or LOGS_DIR) / job_id + return Path(logs_dir or get_logs_dir()) / job_id def job_log_path(job_id: str, kind: str, logs_dir: Optional[str] = None) -> Path: @@ -576,7 +586,7 @@ def iter_logged_events(job_id: str, logs_dir: Optional[str] = None): def list_logged_jobs(logs_dir: Optional[str] = None) -> List[Dict[str, Any]]: """Return one meta record per job directory under the logs root, oldest first. Falls back to ``{"job_id": }`` when meta.json is missing.""" - base = Path(logs_dir or LOGS_DIR) + base = Path(logs_dir or get_logs_dir()) out: List[Dict[str, Any]] = [] if not base.exists(): return out diff --git a/.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py b/.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py index 58f6170..4e57783 100755 --- a/.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py +++ b/.agents/skills/multi-agent-mux-delegate-job/scripts/registry.py @@ -195,7 +195,7 @@ def get_feedback(job_id: str, registry_dir: str = DEFAULT_REGISTRY_DIR) -> str: # 1) Try the unified audit log first (ndjson) since it's written synchronously by the subscriber try: import mqtt_common - logs_dir = mqtt_common.LOGS_DIR + logs_dir = mqtt_common.get_logs_dir() events = list(mqtt_common.iter_logged_events(job_id, logs_dir)) for e in reversed(events): if e.get("source_event") in ("completed", "error"): @@ -386,7 +386,7 @@ def main(argv: Optional[List[str]] = None) -> int: def _cmd_logs(args) -> int: """Pretty-print one job's events.ndjson, or summarise all logged jobs.""" - logs_dir = args.logs_dir or mqtt_common.LOGS_DIR + logs_dir = args.logs_dir or mqtt_common.get_logs_dir() if args.list_all: jobs = mqtt_common.list_logged_jobs(logs_dir) diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 64c89e0..2ad6585 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -1,9 +1,9 @@ # ๐Ÿ› ๏ธ Multi-Agent Mux ์ข…ํ•ฉ ๊ฐœ์„  ๋ฐ ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ ๋ฐฑ๋กœ๊ทธ (`IMPROVEMENTS.md`) -- **์ตœ์ข… ๊ฐฑ์‹ ์ผ**: 2026-08-17 (B-13/Stage 2 ์…€ํ”„ ํ˜ธ์ŠคํŒ… ๋ฃจํ”„ ๋Ÿฐํƒ€์ž„ ํ”„๋ฆฌ์ฆˆ ์Šค๋ƒ…์ƒท ์™„๋ฃŒ, B-10 ์™„๋ฃŒ, B-5 ์ข…๊ฒฐ, C-6 ์™„๋ฃŒ, ์ „์ฒด 271/271 ํšŒ๊ท€ ํ†ต๊ณผ ๋ฐ˜์˜) +- **์ตœ์ข… ๊ฐฑ์‹ ์ผ**: 2026-08-17 (B-9 ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฃจํŠธ ์ง€์—ฐ ํ‰๊ฐ€ ์™„๋ฃŒ, B-13/Stage 2 ์™„๋ฃŒ, B-10 ์™„๋ฃŒ, B-5 ์ข…๊ฒฐ, C-6 ์™„๋ฃŒ, ์ „์ฒด 276/276 ํšŒ๊ท€ ํ†ต๊ณผ ๋ฐ˜์˜) - **ํ†ตํ•ฉ ๊ด€๋ฆฌ ๋Œ€์ƒ**: ๊ธฐ์กด `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` -- **์ด ์ถ”์  ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ**: **2๊ฑด** (์•„ํ‚คํ…์ฒ˜ 1๊ฑด, ์—ฃ์ง€์ผ€์ด์Šค 1๊ฑด, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ 0๊ฑด, ๋ ˆ๊ฑฐ์‹œ ์ž”์žฌ 0๊ฑด) -- **์™„๋ฃŒ๋œ ๊ณผ์ œ**: **23๊ฑด** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-10, B-13, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup) +- **์ด ์ถ”์  ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ**: **1๊ฑด** (์•„ํ‚คํ…์ฒ˜ 1๊ฑด, ์—ฃ์ง€์ผ€์ด์Šค 0๊ฑด, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ 0๊ฑด, ๋ ˆ๊ฑฐ์‹œ ์ž”์žฌ 0๊ฑด) +- **์™„๋ฃŒ๋œ ๊ณผ์ œ**: **24๊ฑด** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-9, B-10, B-13, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup) --- @@ -67,10 +67,7 @@ --- -## 2. ๐ŸŸ  ์—ฃ์ง€ ์ผ€์ด์Šค ๋ฐ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ (Edge-case Bugs โ€” 1๊ฑด) - -### **B-9: `LOGS_DIR` import ์‹œ์  cwd ๊ณ ์ •** -- `mqtt_common.py` ๋ชจ๋“ˆ ๋กœ๋“œ ์‹œ์ ์˜ cwd๋กœ ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฒฝ๋กœ๊ฐ€ 1ํšŒ ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค. +## 2. ๐ŸŸ  ์—ฃ์ง€ ์ผ€์ด์Šค ๋ฐ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ (Edge-case Bugs โ€” 0๊ฑด โ€” ์ „์› ์™„๋ฃŒ) --- @@ -82,7 +79,15 @@ --- -## 5. ๐ŸŽ‰ ์™„๋ฃŒ๋œ ๊ณผ์ œ (Completed Tasks โ€” 23๊ฑด) +## 5. ๐ŸŽ‰ ์™„๋ฃŒ๋œ ๊ณผ์ œ (Completed Tasks โ€” 24๊ฑด) + +### **B-9 (P4-1): `LOGS_DIR` import ์‹œ์  cwd ๊ณ ์ • ํ•ด์†Œ** โ€” โœ… ์™„๋ฃŒ +- `mqtt_common.LOGS_DIR` ์ด ๋ชจ๋“ˆ import ์‹œ์ ์˜ `os.getcwd()` ๋กœ ์ ˆ๋Œ€ํ™”๋˜์–ด, ์ดํ›„ ํ”„๋กœ์„ธ์Šค๊ฐ€ `chdir` ํ•˜๋ฉด ๊ฐ์‚ฌ ๋กœ๊ทธ๊ฐ€ ์˜› ๊ฒฝ๋กœ์— ๊ณ„์† ์Œ“์ด๋˜ ๋ฌธ์ œ๋ฅผ ํ•ด์†Œํ–ˆ์Šต๋‹ˆ๋‹ค(์‹ค์ธก ์žฌํ˜„). ๊ฒฝ๋กœ ํ•ด์„์„ ํ˜ธ์ถœ ์‹œ์ ์œผ๋กœ ๋ฏธ๋ฃจ๋Š” `get_logs_dir()` ๋ฅผ ๋„์ž…ํ•˜๊ณ  ๋ชจ๋“ˆ ์ „์—ญ ๋Œ€์ž…์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. +- **ํ•˜์œ„ ํ˜ธํ™˜**: PEP 562 ๋ชจ๋“ˆ `__getattr__` ๋กœ `mqtt_common.LOGS_DIR` ์†์„ฑ ์ ‘๊ทผ์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋˜ ๋™์ ์œผ๋กœ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ๋‚ด `from mqtt_common import LOGS_DIR` ์‚ฌ์šฉ์€ 0๊ฑด์ž„์„ ์ „์ˆ˜ ํ™•์ธํ–ˆ์œผ๋ฏ€๋กœ ํ˜ธํ™˜ ํ‘œ๋ฉด์ด 100% ๋ฎ์ž…๋‹ˆ๋‹ค. +- **๊ฐ€์‹œ์„ฑ & ํƒ์ƒ‰์„ฑ**: PEP 562 `__dir__` ์„ ํ•จ๊ป˜ ์ •์˜ํ•ด `dir(mqtt_common)` ๋ฐ ํƒญ ์™„์„ฑ์—์„œ `LOGS_DIR` ์ด ๊ณ„์† ๋ณด์ด๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค(`hasattr` ์€ `__getattr__` ๋งŒ์œผ๋กœ๋„ ๋™์ž‘ํ•˜๋ฏ€๋กœ ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค). +- **๋ถ€์ˆ˜ ๊ฐœ์„ **: `DELEGATE_JOB_LOGS_DIR` ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์ด์ œ ์‹คํ–‰ ์ค‘ ๋ณ€๊ฒฝ๊นŒ์ง€ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค(์ข…์ „์—๋Š” import ์ดํ›„ ๋ณ€๊ฒฝ์ด ๋ฌด์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค). +- ๊ฐ™์€ ํŒŒ์ผ์˜ `DEFAULT_REGISTRY_DIR` ์€ ์ƒ๋Œ€ ๋ฌธ์ž์—ด๋กœ ๋‚จ์•„ ์žˆ์–ด ์• ์ดˆ์— ์ด ๊ฒฐํ•จ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค โ€” B-9 ์˜ ๋ณธ์งˆ์€ "ํ•จ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ **์ ˆ๋Œ€ํ™” ์‹œ์ **"์ด์—ˆ์Šต๋‹ˆ๋‹ค. +- ํšŒ๊ท€ ๊ฐ€๋“œ 5์ข…์„ ์‹ ์„คํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ณ„์ธต์€ best-effort `except Exception` ์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ์‚ผํ‚ค๋ฏ€๋กœ, ์ž˜๋ชป๋œ ์ˆ˜์ •์€ **๋ฌด์Œ ๋กœ๊ทธ ์†Œ์‹ค + ์ „ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ**๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค(์‹ค์ธก). ์ด์— ๊ฐ€๋“œ ํ•˜๋‚˜๋Š” ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ผ **์‹ค์ œ ํŒŒ์ผ ์ƒ์„ฑ**์„ ๋‹จ์–ธํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ### **B-13 (Stage 2): ์…€ํ”„ ํ˜ธ์ŠคํŒ… ๋ฃจํ”„ ๋Ÿฐํƒ€์ž„ ํ”„๋ฆฌ์ฆˆ ์Šค๋ƒ…์ƒท** โ€” โœ… ์™„๋ฃŒ - ๋ฃจํ”„ ๊ธฐ๋™ ์‹œ `.agents/skills/` ๋ฅผ `$TMPDIR` ์˜ ์ž„์‹œ ๋””๋ ‰ํ„ฐ๋ฆฌ์— 1ํšŒ ๋™๊ฒฐํ•˜๊ณ  ๊ทธ ์Šค๋ƒ…์ƒท์—์„œ ์žฌ์‹คํ–‰(`exec`)ํ•˜๋„๋ก ํ•˜์—ฌ, ํ„ด ๋„์ค‘ Worker ๊ฐ€ ํ”„๋ ˆ์ž„์›Œํฌ ์Šคํ‚ฌ์„ ํŽธ์ง‘ํ•ด๋„ ์ง„ํ–‰ ์ค‘์ธ ๋ฃจํ”„๊ฐ€ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒํƒœยท์ €์žฅ์†Œ ๊ฒฝ๋กœ๋Š” `MAM_REAL_ROOT`/`WORKSPACE_ROOT` ๋กœ ์‹ค์ œ ๋ฃจํŠธ๋ฅผ ๊ณ„์† ๊ฐ€๋ฆฌํ‚ค๋ฏ€๋กœ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌยท๋ฝยทdiff ์ˆ˜์ง‘ ๋™์ž‘์€ ์ข…์ „๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. @@ -248,7 +253,7 @@ | **P3-2** | **B-10** | tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ ๋ฐ UUID ๊ฒฝ๋กœ PyYAML ํƒˆ์˜์กด (Option A) **(โœ… ์™„๋ฃŒ โ€” tests/test_tier1_unit.py ๊ฐ€๋“œ 3๊ฑด ์‹ ์„ค, ์ „์ฒด 266/266 PASS)** | ์ค‘ | A-4 M2 | | **P3-3** | **C-3b** | `isolation.root` 4๊ฐœ ์†Œ๋น„์ž ์™„์ „ ํ๊ธฐ (Option B ์ฑ„ํƒ) **(โœ… ์™„๋ฃŒ โ€” ์ „์ฒด 259/259 PASS)** | ์†Œ | A-4 M2 | | **B-13** | **Stage 2** | ์…€ํ”„ ํ˜ธ์ŠคํŒ… ๋ฃจํ”„ ๋Ÿฐํƒ€์ž„ ํ”„๋ฆฌ์ฆˆ ์Šค๋ƒ…์ƒท ๋ฐ ์ด์ค‘ ๋ฃจํŠธ ๊ฒฉ๋ฆฌ **(โœ… ์™„๋ฃŒ โ€” tests/test_o3_scoped_guard.py 5๊ฑด ๊ฐ€๋“œ ์‹ ์„ค, ์ „์ฒด 271/271 PASS)** | ์ค‘ | โ€” | -| **P4-1** | **B-9** | ๊ธฐ๋ณธ๊ฐ’ ํ•œ์ •. `logs_dir` ์ธ์žยท`DELEGATE_JOB_LOGS_DIR` ๋‘ ๊ฐ€์ง€ ํšŒํ”ผ ์ˆ˜๋‹จ ์กด์žฌ | ๊ทน์†Œ | โ€” | +| **P4-1** | **B-9** | ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฃจํŠธ ์ง€์—ฐ ํ‰๊ฐ€ ๋ฐ `__getattr__`/`__dir__` ๋™์  ๋ณ„์นญ **(โœ… ์™„๋ฃŒ โ€” tests/test_tier1_unit.py 5๊ฑด ๊ฐ€๋“œ ์‹ ์„ค, ์ „์ฒด 276/276 PASS)** | ๊ทน์†Œ | โ€” | | **P5-1** | **A-2** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ๋ฐ HMAC ๊ฒ€์ฆ ๋ณด์™„ (๐Ÿ“Œ *์‚ฌ์šฉ์ž ์ง€์นจ: ์ฐจํ›„ ์ „์šฉ MQTT ๋ธŒ๋กœ์ปค ์„œ๋น™ ํ™˜๊ฒฝ ๊ตฌ์ถ• ์‹œ์ ์— ์ง„ํ–‰*) | ์ค‘ (3ํŒŒ์ผ) | ์ „์šฉ ๋ธŒ๋กœ์ปค | | **์ข…๊ฒฐ** | **B-5** | `df -P` ํด๋ฐฑ ์ •์ƒ ๋™์ž‘ ์‹ค์ธก ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ ์™„๋ฃŒ **(โœ… ์™„๋ฃŒ/์ข…๊ฒฐ โ€” tests/test_tier1_unit.py test_stop_check_is_nfs_local)** | โ€” | โ€” | diff --git a/VERSIONS.md b/VERSIONS.md index 7e2f1e8..d2dc8fe 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -87,6 +87,14 @@ - ํšŒ๊ท€ ๊ฐ€๋“œ 5์ข… ์‹ ์„ค โ€” argv ๋ณด์กด, ํŒŒ์† ๋ž˜ํผ ๋ฉด์—ญ, ์Šคํ‚ฌ ํŠธ๋ฆฌ ๋ฌด์˜ค์—ผ(B-6 ๊ฒฝ๊ณ„), ๋ฝ ํ•ด์ œ ๋ฐ ์Šค๋ƒ…์ƒท ์ •๋ฆฌ(B-12 ๊ฒฝ๊ณ„), ๋น„ํ™œ์„ฑํ™” ์Šค์œ„์น˜. - ํšŒ๊ท€ ๋ฐ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ: **271/271 PASS (100%)** ๋‹ฌ์„ฑ. +#### 9. ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฃจํŠธ ์ง€์—ฐ ํ‰๊ฐ€ (B-9 / P4-1) +- `mqtt_common.LOGS_DIR` ์˜ import ์‹œ์  cwd ๊ณ ์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ํ˜ธ์ถœ ์‹œ์ ์— ํ•ด์„ํ•˜๋Š” `get_logs_dir()` ๋ฅผ ๋„์ž…. `chdir` ์ดํ›„์—๋„ ๊ฐ์‚ฌ ๋กœ๊ทธ๊ฐ€ ํ˜„์žฌ ์›Œํฌ์ŠคํŽ˜์ด์Šค์— ์ •ํ™•ํžˆ ๊ธฐ๋ก๋จ. +- PEP 562 ๋ชจ๋“ˆ `__getattr__` ๋กœ `LOGS_DIR` ์†์„ฑ ์ ‘๊ทผ ํ•˜์œ„ ํ˜ธํ™˜ ์œ ์ง€(๋™์  ํ‰๊ฐ€). +- PEP 562 `__dir__` ๋ณ‘ํ–‰ ์ •์˜๋กœ `dir()`ยทํƒญ ์™„์„ฑ ๊ฐ€์‹œ์„ฑ ์œ ์ง€. +- `DELEGATE_JOB_LOGS_DIR` ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์‹คํ–‰ ์ค‘ ๋ณ€๊ฒฝ๊นŒ์ง€ ๋ฐ˜์˜. +- ํšŒ๊ท€ ๊ฐ€๋“œ 5์ข… ์‹ ์„ค โ€” cwd ์ถ”์ข…, ์‹ค์ œ ํŒŒ์ผ ์ƒ์„ฑ, ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋™์  ๋ฐ˜์˜, ์ „์—ญ ์žฌ๋„์ž… ์ฐจ๋‹จ, dir() ํƒ์ƒ‰์„ฑ. +- ํšŒ๊ท€ ๋ฐ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ: **276/276 PASS (100%)** ๋‹ฌ์„ฑ. + --- ### ๐Ÿ› ๏ธ `v1.4.0` โ€” Stability, Cleanup & Safe Job Delegation (2026-08-16) diff --git a/tests/test_tier1_unit.py b/tests/test_tier1_unit.py index d5ff662..cfa8b27 100644 --- a/tests/test_tier1_unit.py +++ b/tests/test_tier1_unit.py @@ -402,3 +402,74 @@ def test_b10_find_workspace_uuid_runs_without_pyyaml(tmp_path): assert r.returncode == 0, f"resolution path still needs PyYAML: {r.stderr}" assert "yaml" not in r.stderr.lower(), f"PyYAML touched at runtime: {r.stderr}" + +# =========================================================================== +# B-9: LOGS_DIR import-time cwd binding resolution regression guards +# =========================================================================== + +def test_b9_logs_dir_follows_cwd_changes(mam_sandbox, tmp_path, monkeypatch): + """B-9: the audit-log root must be resolved per call, not frozen at import.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR", raising=False) + a = tmp_path / "a"; b = tmp_path / "b" + a.mkdir(); b.mkdir() + + # realpath on both sides: pytest's tmp_path happens to be pre-resolved today, + # but relying on that is an undocumented dependency (C1'). + def logs_under(p): + return os.path.realpath(os.path.join(str(p), ".mam", "delegate_job_logs")) + + monkeypatch.chdir(a) + assert os.path.realpath(mq.get_logs_dir()) == logs_under(a) + monkeypatch.chdir(b) + assert os.path.realpath(mq.get_logs_dir()) == logs_under(b) + # the compat alias must follow too (T1: a surviving global fails here) + assert os.path.realpath(mq.LOGS_DIR) == logs_under(b) + + +def test_b9_audit_log_lands_under_the_current_cwd(mam_sandbox, tmp_path, monkeypatch): + """B-9/T3: assert the FILE appears โ€” a swallowed NameError must not pass.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR", raising=False) + monkeypatch.chdir(tmp_path) + mq.init_job_log("b9job", {"status": "pending"}) + assert (tmp_path / ".mam" / "delegate_job_logs" / "b9job" / "meta.json").exists(), \ + "audit log did not land under the current cwd (the best-effort handler may have swallowed an error)" + + +def test_b9_logs_dir_env_override_is_dynamic(mam_sandbox, tmp_path, monkeypatch): + """B-9: DELEGATE_JOB_LOGS_DIR must be honoured at call time, both ways.""" + mq = get_mqtt_common(mam_sandbox) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DELEGATE_JOB_LOGS_DIR", "/tmp/b9-override") + assert mq.get_logs_dir() == "/tmp/b9-override" + monkeypatch.delenv("DELEGATE_JOB_LOGS_DIR") + # equality, not inequality โ€” clearing the env must restore the cwd default (Rev.2 ยง3) + assert os.path.realpath(mq.get_logs_dir()) == \ + os.path.realpath(os.path.join(str(tmp_path), ".mam", "delegate_job_logs")) + + +def test_b9_no_module_level_logs_dir_binding(): + """B-9/T1: a surviving module global would make __getattr__ dead code.""" + import ast, pathlib + src = (pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills" + / "multi-agent-mux-delegate-job" / "scripts" / "mqtt_common.py") + tree = ast.parse(src.read_text()) + for node in tree.body: # module scope only + if isinstance(node, ast.Assign): + for t in node.targets: + assert not (isinstance(t, ast.Name) and t.id == "LOGS_DIR"), \ + f"line {node.lineno}: module-level LOGS_DIR binding shadows __getattr__ (B-9/T1)" + elif isinstance(node, ast.AnnAssign): # C2-b: LOGS_DIR: str = ... parses as AnnAssign + assert not (isinstance(node.target, ast.Name) and node.target.id == "LOGS_DIR"), \ + f"line {node.lineno}: annotated module-level LOGS_DIR binding shadows __getattr__ (B-9/T1)" + + +def test_b9_logs_dir_stays_discoverable(mam_sandbox): + """B-9/C2-a: PEP 562 __dir__ keeps LOGS_DIR visible to dir() and tooling.""" + mq = get_mqtt_common(mam_sandbox) + assert "LOGS_DIR" in dir(mq) + assert hasattr(mq, "LOGS_DIR") # true via __getattr__ even without __dir__ + assert dir(mq).count("LOGS_DIR") == 1 # set-based __dir__ must not duplicate + +