diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-00334786.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-00334786.md new file mode 100644 index 0000000..380ee12 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-00334786.md @@ -0,0 +1,435 @@ +# ๐Ÿ“ ๊ตฌํ˜„ ๊ณ„ํš์„œ Rev.2 โ€” B-10 (P3-2): `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ (Option A) + +- **Job ID**: `104b94c8` (Rev.1 = `00334786`) +- **Planner**: claude (session: `herdr:canary-projects-multi-agent-mux-creator-claude`) +- **Role**: Planner (`MULTI_AGENT_RULES.md` ยง1 โ€” ๋ณธ ์ž‘์—…์—์„œ ์ €์žฅ์†Œ ์ฝ”๋“œ 0๊ฑด ์ˆ˜์ •) +- **๋ฐ˜์˜ ๋Œ€์ƒ Challenge**: `26f5d224` (agy, Worker / Plan Reviewer) โ€” `[VERDICT: PASS WITH CHALLENGE]` +- **๊ธฐ์ค€ ์ปค๋ฐ‹**: `7e21077` (`refactor`, ์ž‘์—… ํŠธ๋ฆฌ clean) + +--- + +## 0. ์š”์•ฝ + +**Challenge 2๊ฑด ๋ชจ๋‘ ํƒ€๋‹นํ•ฉ๋‹ˆ๋‹ค. ์ „๋ฉด ์ˆ˜์šฉํ•ฉ๋‹ˆ๋‹ค.** ๊ฒฉ๋ฆฌ ํด๋ก ์—์„œ Rev.1 ์˜ ๊ฐ€๋“œ ์ฝ”๋“œ๋ฅผ **์›๋ฌธ ๊ทธ๋Œ€๋กœ ์‹คํ–‰**ํ•ด ๋‘ ๊ฒฐํ•จ์„ ์žฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + +๊ทธ๋ฆฌ๊ณ  ์ฑŒ๋ฆฐ์ €์˜ ๊ถŒ๊ณ ์•ˆ #1 ์„ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•ด ๋ณด๋Š” ๊ณผ์ •์—์„œ **์ƒ์‚ฐ ์ฝ”๋“œ ๊ฒฐํ•จ 1๊ฑด์„ ์ƒˆ๋กœ ๋ฐœ๊ฒฌ**ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ด๋ฒˆ Rev.2 ์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์‚ฐ์ถœ์ž…๋‹ˆ๋‹ค. + +> **์‹ ๊ทœ ๋ฐœ๊ฒฌ**: `verify_session.py:10` ์ด `import os, sys, json, sqlite3, yaml` ๋กœ **yaml ์„ ์ฆ‰์‹œ import** ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜(`mam_orchestrator_uuids`)๋Š” `find_workspace_uuid_main()` ์ด `:38` ์—์„œ **tier ๋กœ์ง๋ณด๋‹ค ๋จผ์ €** ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ **tier-3 ์„ ์ œ๊ฑฐํ•ด๋„ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๋Š” ์—ฌ์ „ํžˆ PyYAML ์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค.** ๋ธŒ๋ฆฌํ”„์˜ ๋ชฉํ‘œ("remove PyYAML dependency from workspace_uuid.py")๋Š” *ํŒŒ์ผ* ๋‹จ์œ„๋กœ๋Š” ๋‹ฌ์„ฑ๋˜์ง€๋งŒ *์‹คํ–‰ ๊ฒฝ๋กœ* ๋‹จ์œ„๋กœ๋Š” ๋‹ฌ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +์ด๋Š” B-10 ํ•ญ๋ชฉ (b) ๊ฐ€ ์›๋ž˜ `lib.sh`/`load_state_json` ์— ๋Œ€ํ•ด ์„œ์ˆ ํ–ˆ๋˜ **๋ฐ”๋กœ ๊ทธ ๊ฒฐํ•จ ํŒจํ„ด์ด ๋‹ค๋ฅธ ํŒŒ์ผ์— ๋ฏธ์ˆ˜์ • ์ƒํƒœ๋กœ ๋‚จ์•„ ์žˆ๋˜ ๊ฒƒ**์ž…๋‹ˆ๋‹ค. `state.py:34` ๊ฐ€ ์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅธ ์„ ๋ก€(๋ถ„๊ธฐ ๋‚ด๋ถ€ import)๋ฅผ ์ œ๊ณตํ•˜๋ฏ€๋กœ 1์ค„๋กœ ๊ต์ •๋ฉ๋‹ˆ๋‹ค. **๋‹จ๊ณ„ 4 ๋กœ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.** + +| ํ•ญ๋ชฉ | Rev.1 | Rev.2 | +|---|---|---| +| C2 `pathlib` NameError | ์กด์žฌ | **์ˆ˜์ •** | +| C1 ๊ฐ€๋“œ ๊ณตํ—ˆ์„ฑ | `import lib_py.workspace_uuid` โ€” ๋ฒ ์ด์Šค๋ผ์ธ์—์„œ๋„ ํ†ต๊ณผ | **AST ๊ฒ€์‚ฌ + ์‹คํ–‰ ๊ฒ€์‚ฌ 2์ข…์œผ๋กœ ๊ต์ฒด** | +| ๊ฐ€๋“œ ์ˆ˜ | 2 | **3** | +| `verify_session.py:10` ์ฆ‰์‹œ yaml import | **๋ฏธ์ธ์ง€** | **๋‹จ๊ณ„ 4 ์‹ ์„ค** | +| ๋ฎคํ…Œ์ด์…˜ ๊ฒ€์ฆ | ๊ณ„ํš๋งŒ ์ œ์‹œ(M1~M3) | **5์ข… ์‹ค์ธก ์™„๋ฃŒ(M1ยทM2ยทM3aยทM3bยทM4)** | +| ์ œ๊ฑฐ ๋‹จ๊ณ„ ์ž์ฒด์˜ ์‹คํ–‰ ๊ฒ€์ฆ | ๋ฏธ์‹ค์‹œ | **ํด๋ก ์— ์„ ์ ์šฉ ํ›„ ๊ตฌ๋ฌธยท๊ฐ€๋“œยท์ „์ฒด ํšŒ๊ท€ ํ™•์ธ** | + +--- + +## 1. Challenge ํŒ์ • โ€” 2๊ฑด ๋ชจ๋‘ ์ˆ˜์šฉ (์‹คํ–‰์œผ๋กœ ์žฌํ˜„) + +### 1.1 C2 โ€” `pathlib` NameError (ํ™•์ธ) + +Rev.1 ยง5.1 ์˜ ๋‘ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ๋Š” ์ง€์—ญ import ๊ฐ€ `subprocess, sys, os` ๋ฟ์ธ๋ฐ `pathlib.Path` ๋ฅผ ์”๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ๊ฐ€ `pathlib` ์„ import ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ์€ **์ž๊ธฐ ํ•จ์ˆ˜ ์Šค์ฝ”ํ”„**์ด๊ณ , `tests/test_tier1_unit.py:1-8` ์—๋„ ์ตœ์ƒ์œ„ `import pathlib` ์ด ์—†์Šต๋‹ˆ๋‹ค(ํ™•์ธ). + +Rev.1 ๊ฐ€๋“œ๋ฅผ ํด๋ก ์— ์›๋ฌธ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ ์‹คํ–‰: + +``` +> skills = str(pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills") +E NameError: name 'pathlib' is not defined +tests/test_tier1_unit.py:370: NameError +``` + +์ฑŒ๋ฆฐ์ €๊ฐ€ ์˜ˆ์ธกํ•œ ๊ทธ ์ค„์—์„œ ์ •ํ™•ํžˆ ์žฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. **๋‹จ์ˆœ ๋ˆ„๋ฝ์ด๋ฉฐ ์ œ ์‹ค์ˆ˜์ž…๋‹ˆ๋‹ค.** + +### 1.2 C1 โ€” ๊ฐ€๋“œ๊ฐ€ ๋ฒ ์ด์Šค๋ผ์ธ์—์„œ ํ†ต๊ณผ(๊ณตํ—ˆ) (ํ™•์ธ) + +`pathlib` ๋งŒ ๊ณ ์น˜๊ณ  **tier-3 ์ด ๊ทธ๋Œ€๋กœ ์‚ด์•„ ์žˆ๋Š” ๋ฏธ์ˆ˜์ • ๋ฒ ์ด์Šค๋ผ์ธ**์—์„œ ๋‹ค์‹œ ์‹คํ–‰: + +``` +tests/test_tier1_unit.py::test_b10_no_agent_identities_reader_in_production FAILED โ† ์ •์ƒ (offender 9๊ฑด ์—ด๊ฑฐ) +tests/test_tier1_unit.py::test_b10_workspace_uuid_needs_no_pyyaml PASSED โ† ๊ณตํ—ˆ +1 failed, 1 passed +``` + +๊ฐ€๋“œ 1 ์€ ์ œ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค(์ œ๊ฑฐ ์ „์ด๋ฏ€๋กœ ์‹คํŒจ). **๊ฐ€๋“œ 2 ๋Š” ์ œ๊ฑฐ๊ฐ€ ์ผ์–ด๋‚˜์ง€ ์•Š์•˜๋Š”๋ฐ๋„ ํ†ต๊ณผ**ํ•ฉ๋‹ˆ๋‹ค โ€” ์ฑŒ๋ฆฐ์ € ์ง€์ ๋Œ€๋กœ `import lib_py.workspace_uuid` ๋Š” `find_workspace_uuid_main()` ์„ ์‹คํ–‰ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ `:100` ์˜ ์ง€์—ฐ import ์— ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. + +**์ถ”๊ฐ€ ์‹ค์ธก โ€” ๊ณตํ—ˆ์„ฑ์˜ ์ •ํ™•ํ•œ ๋ฒ”์œ„**: Rev.1 ์ด ์ œ์•ˆํ–ˆ๋˜ ๋ฎคํ…Œ์ด์…˜ M3(์ตœ์ƒ์œ„ `import yaml` ์ถ”๊ฐ€)์€ ์‹ค์ œ๋กœ๋Š” ์žก์Šต๋‹ˆ๋‹ค. ์žก์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ์€ **์ด ์ €์žฅ์†Œ์— ์‹ค์ œ๋กœ ์กด์žฌํ–ˆ๋˜ ํ˜•ํƒœ**, ์ฆ‰ ํ•จ์ˆ˜ ๋‚ด๋ถ€ ์ง€์—ฐ import ์ž…๋‹ˆ๋‹ค. + +| ๋ฎคํ…Œ์ด์…˜ | Rev.1 ๊ฐ€๋“œ 2 | +|---|---| +| M3a โ€” ์ตœ์ƒ์œ„ `import yaml` | **FAIL** โœ… ์žก์Œ | +| M3b โ€” ํ•จ์ˆ˜ ๋‚ด๋ถ€ ์ง€์—ฐ `import yaml` (C1 ์ด ์ง€๋ชฉํ•œ ํ˜•ํƒœ) | **PASS** โŒ ๋ชป ์žก์Œ | + +์ฆ‰ ์ œ๊ฐ€ ์„ค๊ณ„ํ•œ ๊ฐ€๋“œ๋Š” **์ œ๊ฐ€ ์ƒ์ƒํ•œ ๊ฒฐํ•จ ํ˜•ํƒœ๋งŒ** ๋ฐฉ์–ดํ•˜๊ณ  **์‹ค์ œ๋กœ ์žˆ์—ˆ๋˜ ํ˜•ํƒœ**๋Š” ๋†“์นฉ๋‹ˆ๋‹ค. ์ฑŒ๋ฆฐ์ € ์ง€์ ์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 2. ์ฑŒ๋ฆฐ์ € ๊ถŒ๊ณ ์•ˆ ํ‰๊ฐ€ + +์ฑŒ๋ฆฐ์ €๋Š” ๋‘ ๊ฐ€์ง€๋ฅผ ๊ถŒ๊ณ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +### 2.1 ๊ถŒ๊ณ  #2 (AST/ํ…์ŠคํŠธ ๊ฒ€์‚ฌ) โ€” ์ฑ„ํƒ, AST ๋กœ ์ •๋ฐ€ํ™” + +ํ…์ŠคํŠธ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ(`"yaml" not in source`)๋Š” `YAML_PATH` ๊ฐ™์€ ์ •๋‹นํ•œ ์‹๋ณ„์ž์— ๊ฑธ๋ ค ํ–ฅํ›„ ์˜คํƒ์„ ๋ƒ…๋‹ˆ๋‹ค. **AST ๋กœ `Import`/`ImportFrom` ๋…ธ๋“œ๋งŒ** ๊ฒ€์‚ฌํ•˜๋ฉด ์ค‘์ฒฉ ๊นŠ์ด์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ •ํ™•ํžˆ ์žก์Šต๋‹ˆ๋‹ค. + +### 2.2 ๊ถŒ๊ณ  #1 (mock env ๋กœ `find_workspace_uuid_main()` ์‹คํ–‰) โ€” ์ฑ„ํƒ, **๋‹จ ๊ทธ๋Œ€๋กœ๋Š” ์˜คํƒ** + +๋ฐฉํ–ฅ์€ ์˜ณ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ **๋ช…์„ธ๋œ ํ˜•ํƒœ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์™„๋ฒฝํ•œ B-10 ๊ตฌํ˜„ ์œ„์—์„œ๋„ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.** ์ฑŒ๋ฆฐ์ €๊ฐ€ ์ œ์‹œํ•œ 4๊ฐœ ํ™˜๊ฒฝ๋ณ€์ˆ˜(`WS_ABS`, `AGENT`, `MAM_STATE_JSON`, `YAML_PATH`)๋ฅผ ๊ฐ–์ถ”๊ณ  noyaml ์Šคํ… ํ•˜์—์„œ ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ: + +``` +AssertionError: resolution path still needs PyYAML: + File ".../lib_py/workspace_uuid.py", line 38, in find_workspace_uuid_main + orchestrator_ids = set(mam_orchestrator_uuids()) + File ".../lib_py/verify_session.py", line 10, in mam_orchestrator_uuids +ImportError: PyYAML absent (stub) +``` + +์‹คํŒจ ์›์ธ์€ `workspace_uuid.py` ๊ฐ€ ์•„๋‹ˆ๋ผ **`verify_session.py`** ์ž…๋‹ˆ๋‹ค โ€” ยง3 ์˜ ์‹ ๊ทœ ๋ฐœ๊ฒฌ์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. ๊ถŒ๊ณ  #1 ์€ ๊ทธ ๊ฒฐํ•จ์„ ํ•จ๊ป˜ ๊ณ ์นœ ๋’ค์—์•ผ ์˜๋ฏธ ์žˆ๋Š” ๊ฐ€๋“œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ณ„ํš์€ **๋‘˜ ๋‹ค** ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 3. ๐Ÿ†• ์‹ ๊ทœ ๋ฐœ๊ฒฌ โ€” `verify_session.py:10` ์˜ ์ฆ‰์‹œ `yaml` import + +### 3.1 ๊ฒฐํ•จ + +```python +# verify_session.py:6-11 +def mam_orchestrator_uuids(): + global _MAM_ORC_CACHE + if _MAM_ORC_CACHE is not None: + return _MAM_ORC_CACHE + import os, sys, json, sqlite3, yaml # โ† :10 yaml ์„ ๋ฌด์กฐ๊ฑด import + override = os.environ.get("MAM_ORCHESTRATOR_UUIDS") +``` + +`yaml` ์€ ์ด ํ•จ์ˆ˜ ์•ˆ์—์„œ **์‹ค์ œ๋กœ ์“ฐ์ž…๋‹ˆ๋‹ค** โ€” `:48` ์˜ `yaml.safe_load(f)` (YAML ํด๋ฐฑ). ๋ฌธ์ œ๋Š” **import ์œ„์น˜**์ž…๋‹ˆ๋‹ค. `:10` ์€ ํ•จ์ˆ˜ ์ง„์ž… ์ฆ‰์‹œ ์‹คํ–‰๋˜๋ฏ€๋กœ: + +- DB ๋ถ„๊ธฐ๋งŒ ํƒ€๋„ PyYAML ํ•„์š” +- `MAM_ORCHESTRATOR_UUIDS` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์กฐ๊ธฐ ๋ฐ˜ํ™˜ํ•ด๋„ ํ•„์š” (import ๊ฐ€ `:10`, ์˜ค๋ฒ„๋ผ์ด๋“œ ๊ฒ€์‚ฌ๊ฐ€ `:11`) + +**์‹ค์ธก** โ€” ์˜ค๋ฒ„๋ผ์ด๋“œ๋ฅผ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์ฃผ์–ด ์ฆ‰์‹œ ๋ฐ˜ํ™˜์‹œ์ผœ๋„: + +``` +$ PYTHONPATH=:... MAM_ORCHESTRATOR_UUIDS="" python -c "โ€ฆmam_orchestrator_uuids()" +ImportError: PyYAML absent (stub) +``` + +### 3.2 ์™œ B-10 ๋ฒ”์œ„์ธ๊ฐ€ + +`find_workspace_uuid_main()` ์€ `:38` ์—์„œ `mam_orchestrator_uuids()` ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค โ€” **tier-1 ๋ณด๋‹ค๋„ ๋จผ์ €**์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ tier-3 ์„ ์ง€์›Œ๋„ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ ์ „์ฒด๋Š” PyYAML ์„ ์š”๊ตฌํ•œ ์ฑ„ ๋‚จ์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ฆฌํ”„์˜ ๋ชฉํ‘œ๋ฅผ *์‹คํ–‰ ๊ฒฝ๋กœ* ๊ธฐ์ค€์œผ๋กœ ๋‹ฌ์„ฑํ•˜๋ ค๋ฉด ์ด ํ•œ ์ค„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + +๋˜ํ•œ ์ด๊ฒƒ์€ B-10 ํ•ญ๋ชฉ (b) ๊ฐ€ ์„œ์ˆ ํ•œ ๊ฒƒ๊ณผ **๋™์ผํ•œ ๊ฒฐํ•จ ํŒจํ„ด**์ž…๋‹ˆ๋‹ค. (b) ๋Š” `lib.sh`/`load_state_json` ์— ๋Œ€ํ•ด ์ œ๊ธฐ๋˜์—ˆ๊ณ  `state.py` ์ด๊ด€ ๊ณผ์ •์—์„œ ํ•ด์†Œ๋˜์—ˆ๋Š”๋ฐ(Rev.1 ยง1.2), **๊ฐ™์€ ํŒจํ„ด์ด `verify_session.py` ์— ๋‚จ์•„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.** B-10 ์„ "PyYAML ์˜์กด ์™„ํ™”" ๊ณผ์ œ๋กœ ๋‹ซ์œผ๋ฉด์„œ ์ด๊ฑธ ๋‚จ๊ธฐ๋ฉด ํ•ญ๋ชฉ์ด ์ ˆ๋ฐ˜๋งŒ ๋‹ซํž™๋‹ˆ๋‹ค. + +### 3.3 ๊ต์ • โ€” `state.py:34` ์„ ๋ก€๋ฅผ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฆ„ + +```python + import os, sys, json, sqlite3 # :10 โ€” yaml ์ œ๊ฑฐ + ... + if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p): + try: + import yaml # โ† YAML ํด๋ฐฑ ๋ถ„๊ธฐ ์•ˆ์œผ๋กœ + with open(yaml_p) as f: + d_obj = yaml.safe_load(f) or {} +``` + +**์‹ค์ธก ํ™•์ธ**: ์ด ๊ต์ • ํ›„ ยง4 ์˜ ์‹คํ–‰ ๊ฐ€๋“œ๊ฐ€ ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค(๊ต์ • ์ „ FAIL โ†’ ๊ต์ • ํ›„ PASS). + +### 3.4 `lib_py` ์˜ `yaml` import ์ „์ˆ˜ ์กฐ์‚ฌ + +| ์œ„์น˜ | ํŒ์ • | +|---|---| +| `atomic_yaml.py:6` (๋ชจ๋“ˆ ์ตœ์ƒ๋‹จ) | **์ •๋‹น** โ€” ๋ชจ๋“ˆ์˜ ์กด์žฌ ์ด์œ ๊ฐ€ YAML ์ง๋ ฌํ™”์ด๊ณ , ์ด์ค‘ ์ธํ„ฐํ”„๋ฆฌํ„ฐ ์ „๋žต์ƒ ์‹œ์Šคํ…œ python3(PyYAML ๋ณด์œ )์—์„œ๋งŒ ์‹คํ–‰๋จ | +| `state.py:34` (๋ถ„๊ธฐ ๋‚ด๋ถ€) | **์ด๋ฏธ ์˜ฌ๋ฐ”๋ฆ„** โ€” ์ด๋ฒˆ ๊ต์ •์˜ ์„ ๋ก€ | +| `workspace_uuid.py:100` (tier-3 ๋‚ด๋ถ€) | B-10 ๋‹จ๊ณ„ 1 ์—์„œ ์ œ๊ฑฐ | +| **`verify_session.py:10` (ํ•จ์ˆ˜ ์ฆ‰์‹œ)** | **๋‹จ๊ณ„ 4 ์‹ ์„ค** | + +๊ต์ • ํ›„ `lib_py` ์˜ ๋ฌด์กฐ๊ฑด์  PyYAML ์š”๊ตฌ๋Š” `atomic_yaml.py` ํ•˜๋‚˜๋กœ ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 4. ํ™•์ • ํšŒ๊ท€ ๊ฐ€๋“œ โ€” 3์ข…, ๋ฎคํ…Œ์ด์…˜ 5์ข… ์‹ค์ธก ์™„๋ฃŒ + +### 4.1 ํ™•์ • ์ฝ”๋“œ โ€” `tests/test_tier1_unit.py` ์— ์ถ”๊ฐ€ + +```python +def test_b10_no_agent_identities_reader_in_production(): + """B-10: agent_identities has no writer; no production code may read it.""" + import pathlib + root = pathlib.Path(__file__).resolve().parent.parent + targets = [ + root / ".agents" / "skills" / "lib_py" / "workspace_uuid.py", + root / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh", + root / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh", + root / ".agents" / "skills" / "lib.sh", + ] + offenders = [] + for f in targets: + for i, line in enumerate(f.read_text().splitlines(), 1): + if "agent_identities" not in line: + continue + if line.lstrip().startswith("#"): # ๊ธˆ์ง€ ๊ทœ์•ฝ์„ ์„œ์ˆ ํ•˜๋Š” ์ฃผ์„์€ ํ—ˆ์šฉ + continue + offenders.append(f"{f.name}:{i}: {line.strip()}") + assert not offenders, "agent_identities read path resurrected:\n" + "\n".join(offenders) + + +def test_b10_workspace_uuid_has_no_yaml_import(): + """B-10: no `import yaml` anywhere in workspace_uuid.py โ€” top-level OR lazy.""" + import ast, pathlib + src = (pathlib.Path(__file__).resolve().parent.parent + / ".agents" / "skills" / "lib_py" / "workspace_uuid.py") + tree = ast.parse(src.read_text()) + offenders = [] + for node in ast.walk(tree): # ast.walk โ†’ ์ค‘์ฒฉ ๊นŠ์ด ๋ฌด๊ด€ + if isinstance(node, ast.Import): + for a in node.names: + if a.name.split(".")[0] == "yaml": + offenders.append(f"line {node.lineno}: import {a.name}") + elif isinstance(node, ast.ImportFrom): + if (node.module or "").split(".")[0] == "yaml": + offenders.append(f"line {node.lineno}: from {node.module} import ...") + assert not offenders, "PyYAML dependency reintroduced:\n" + "\n".join(offenders) + + +def test_b10_find_workspace_uuid_runs_without_pyyaml(tmp_path): + """B-10: the executed resolution path must not need PyYAML.""" + import subprocess, sys, os, json, pathlib + stub = tmp_path / "noyaml" + (stub / "yaml").mkdir(parents=True) + (stub / "yaml" / "__init__.py").write_text('raise ImportError("PyYAML absent (stub)")\n') + skills = str(pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills") + ws = tmp_path / "ws"; ws.mkdir() + env = os.environ.copy() + env["PYTHONPATH"] = f"{stub}:{skills}" + env["WS_ABS"] = str(ws) + env["AGENT"] = "claude" + env["MAM_STATE_JSON"] = json.dumps({"herdr_sessions": []}) + env["YAML_PATH"] = str(tmp_path / "agent-sessions.yaml") + env["HOME_DIR"] = str(tmp_path) + env["CLAUDE_PROJECT_DIR"] = str(tmp_path / "projects") + r = subprocess.run( + [sys.executable, "-c", + "from lib_py.workspace_uuid import find_workspace_uuid_main; find_workspace_uuid_main()"], + capture_output=True, text=True, env=env) + 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}" +``` + +`env` ๋ฅผ ๋ช…์‹œ ๊ตฌ์„ฑํ•˜๋ฏ€๋กœ ์•ฐ๋น„์–ธํŠธ `PYTHONPATH` ์— ์˜์กดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค(์ง์ „ ๋ผ์šด๋“œ N1 ์žฌ๋ฐœ ๋ฐฉ์ง€). ์ง€์—ญ import ์— `pathlib` ์„ ํฌํ•จ์‹œ์ผœ C2 ๋ฅผ ํ•ด์†Œํ–ˆ์Šต๋‹ˆ๋‹ค. + +### 4.2 ๋ฎคํ…Œ์ด์…˜ ๋งคํŠธ๋ฆญ์Šค โ€” Rev.2 ์—์„œ ์‹ค์ธก + +ํด๋ก ์— ยง5 ๋‹จ๊ณ„ 1~4 ๋ฅผ ์„ ์ ์šฉํ•œ ๋’ค ์ธก์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. + +| # | ๋ฎคํ…Œ์ด์…˜ | ๊ธฐ๋Œ€ | ์‹ค์ธก | +|---|---|---|---| +| โ€” | baseline (์ œ๊ฑฐ + ๊ต์ • ์ ์šฉ) | PASS | **3 passed** โœ… | +| M1 | `workspace_uuid.py` ์— `agent_identities` ์ฝ๊ธฐ ๋ณต์› | ๊ฐ€๋“œ 1 FAIL | **1 failed** โœ… | +| M2 | `reconcile.sh` ์— drift D ์ฝ๊ธฐ ๋ณต์› | ๊ฐ€๋“œ 1 FAIL | **1 failed** โœ… | +| M3a | ์ตœ์ƒ์œ„ `import yaml` | ๊ฐ€๋“œ 2 FAIL | **2 failed** โœ… (์‹คํ–‰ ๊ฐ€๋“œ๋„ ๋™๋ฐ˜ ์‹คํŒจ) | +| **M3b** | **ํ•จ์ˆ˜ ๋‚ด๋ถ€ ์ง€์—ฐ `import yaml`** (C1 ํ˜•ํƒœ) | ๊ฐ€๋“œ 2 FAIL | **1 failed** โœ… **โ† Rev.1 ์ด ๋†“์ณค๋˜ ํ˜•ํƒœ** | +| M4 | `verify_session.py` yaml ์ง€์—ฐ ๊ต์ • ๋˜๋Œ๋ฆผ | ๊ฐ€๋“œ 3 FAIL | **1 failed** โœ… | + +M3b ๊ฐ€ Rev.2 ์˜ ํ•ต์‹ฌ ๊ฐœ์„ ์ž…๋‹ˆ๋‹ค โ€” Rev.1 ๊ฐ€๋“œ์—์„œ๋Š” ์ด ๋ฎคํ…Œ์ด์…˜์ด ํ†ต๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## 5. ๊ตฌํ˜„ ๊ณ„ํš + +### 5.1 ๋‹จ๊ณ„ 1 โ€” `workspace_uuid.py` tier-3 ์ œ๊ฑฐ + +`ai = d.get('agent_identities') โ€ฆ` ๋ถ€ํ„ฐ `print('')` ์ง์ „๊นŒ์ง€ 28์ค„ ์‚ญ์ œ, import ๋ฅผ `import os, sys, json` ์œผ๋กœ ์ถ•์†Œ(`sqlite3` ์€ tier-3 ์™ธ ์‚ฌ์šฉ์ฒ˜ 0๊ฑด). + +> **ํด๋ก  ์‹ค์ธก**: ์‚ญ์ œ ํ›„ `ast.parse` OK, ์ „์ฒด ํšŒ๊ท€ ยง8-9 ์ฐธ์กฐ. + +### 5.2 ๋‹จ๊ณ„ 2 โ€” `reconcile.sh` drift D ์ œ๊ฑฐ + +`# === drift D: stale UUID โ€ฆ ===` ๋ถ€ํ„ฐ `result = {` ์ง์ „๊นŒ์ง€ 35์ค„ ์‚ญ์ œ. `bash -n` OK ํ™•์ธ. + +**์ฃผ์˜**: `glob`/`sqlite3` import ๋Š” **๋‹ค๋ฅธ ๋ถ„๊ธฐ์—์„œ๋„ ์“ฐ์ด๋ฏ€๋กœ ์ œ๊ฑฐํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค**(ํด๋ก  ์‹ค์ธก์—์„œ ์‚ญ์ œ ์—†์ด ์ •์ƒ ๋™์ž‘). + +### 5.3 ๋‹จ๊ณ„ 3 โ€” `stop_session.sh` ์บ์‹œ ์†Œ๊ฑฐ ์ œ๊ฑฐ + +`# agent_identities ๋Š” cache โ€” โ€ฆ` ๋ธ”๋ก 6์ค„ ์‚ญ์ œ. `:164` ์ฃผ์„์„ `tier-1(row) -> tier-2(workspace-scoped disk scan)` ๋กœ ์ •์ •. `bash -n` OK ํ™•์ธ. + +### 5.4 ๐Ÿ†• ๋‹จ๊ณ„ 4 โ€” `verify_session.py:10` yaml ์ง€์—ฐํ™” (ยง3) + +```python +- import os, sys, json, sqlite3, yaml ++ import os, sys, json, sqlite3 +``` + +๊ทธ๋ฆฌ๊ณ  `yaml.safe_load` ๋ฅผ ์“ฐ๋Š” YAML ํด๋ฐฑ `try:` ๋ธ”๋ก ์ฒซ ์ค„์— `import yaml` ์„ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค. **1์ค„ ์ด๋™**์ด๋ฉฐ `state.py:34` ์™€ ๋™์ผํ•œ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค. + +### 5.5 ๋‹จ๊ณ„ 5 โ€” `lib.sh` ์ฃผ์„ ์ •์ • + +```bash +# Resolution order: +# 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id +# (claude_session_id_own / agy_conversation_id_own) +# 2) on-disk scan scoped to this workspace, via the agent adapter's discover() +# Prints the UUID on stdout (empty line if none). Always exits 0. +``` + +`:1326` ์˜ `3-tier` โ†’ `2-tier`, `โ€ฆ -> cwd-matched cache` ์ œ๊ฑฐ. + +### 5.6 ๋‹จ๊ณ„ 6 โ€” ์Šคํ‚ฌ ๋ฌธ์„œ + +- `status/SKILL.md:108` drift D ํ–‰ ์‚ญ์ œ (A/B/C 3์ข…๋งŒ) +- `monitor/SKILL.md:143` ์˜ˆ์‹œ ์ถœ๋ ฅ์˜ `agent_identities.*` ์ค„ ์‚ญ์ œ +- `resume/SKILL.md:50-58` ํ•ด๊ฒฐ ์ˆœ์„œ ๊ต์ฒด โ€” **๊ธฐ์กด ์„œ์ˆ ์ด ์ด๋ฏธ ์˜ค๋ฅ˜**์ž…๋‹ˆ๋‹ค. `agent_identities` ๋ฅผ 1ยท2์ˆœ์œ„ primary ๋กœ ์•ˆ๋‚ดํ•˜๊ณ  ์žˆ์œผ๋‚˜ P0-C ๊ฐ€ ์ด๋ฅผ cache ๋กœ ๊ฐ•๋“ฑํ–ˆ์Šต๋‹ˆ๋‹ค(`update_yaml_resumed.sh:5` ๊ฐ€ ๋ช…์‹œ). ์‹ค์ œ ์ˆœ์„œ๋กœ ๊ต์ฒด: + +``` +1. herdr_sessions[] ํ–‰์˜ per-row own id (claude_session_id_own / agy_conversation_id_own) + โ€” multi-agent-mux-stop ์ด ์ข…๋ฃŒ ์ง์ „ ํ™•์ • ๊ธฐ๋กํ•œ ๊ฐ’ (tier-1, race-free) +2. ์›Œํฌ์ŠคํŽ˜์ด์Šค๋กœ ์Šค์ฝ”ํ”„๋œ ์˜จ๋””์Šคํฌ ์Šค์บ” (์–ด๋Œ‘ํ„ฐ discover()) +๋‘˜ ๋‹ค ๋น„๋ฉด โ†’ ์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค์—๋Š” ์•„์ง ๋Œ€ํ™”๊ฐ€ ์—†์Œ. multi-agent-mux-create ๋กœ. +``` + +**๋ณด์กด**: `resolve_session_id.sh:7` ์˜ `# P0-C: ์ „์—ญ agent_identities ๋ฅผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š”๋‹ค` ๋Š” **๊ธˆ์ง€ ๊ทœ์•ฝ** ์„œ์ˆ ์ด๋ฏ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค(tier-3 ์ œ๊ฑฐ๋กœ ์˜คํžˆ๋ ค ๋” ์ •ํ™•ํ•ด์ง). ๊ฐ€๋“œ 1 ์˜ ์ฃผ์„ ํ—ˆ์šฉ ๊ทœ์น™์ด ์ด๋ฅผ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค. + +--- + +## 6. `adapter.identity_cache_fields` โ€” Option A ํ™•์ • + +Rev.1 ยง4 ์—์„œ ํŒ๋‹จ์„ ์š”์ฒญํ–ˆ๊ณ  **์ฑŒ๋ฆฐ์ €๊ฐ€ ยง3 ํ‘œ์—์„œ "Adopt Option A" ๋กœ ๋™์˜**ํ–ˆ์œผ๋ฏ€๋กœ ํ™•์ •ํ•ฉ๋‹ˆ๋‹ค. + +๋‹จ๊ณ„ 3 ์ด `stop_session.sh` ์˜ ์œ ์ผํ•œ ์ƒ์‚ฐ ์†Œ๋น„์ž๋ฅผ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ, `base.py:57` ์— ๊ทผ๊ฑฐ ์ฃผ์„์„ **๋ฐ˜๋“œ์‹œ** ๋‚จ๊น๋‹ˆ๋‹ค. + +```python + @property + def identity_cache_fields(self) -> tuple: + """agent_identities ์บ์‹œ์˜ ์—์ด์ „ํŠธ๋ณ„ ํ•„๋“œ๋ช…. + B-10(Option A)๋กœ ์บ์‹œ ์ฝ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์ œ๊ฑฐ๋˜์–ด ํ˜„์žฌ ์ƒ์‚ฐ ์†Œ๋น„์ž๋Š” 0๊ฑด์ด์ง€๋งŒ, + ์บ์‹œ ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ๋„์ž…๋˜๋ฉด ์ฆ‰์‹œ ํ•„์š”ํ•œ ์œ ์ผํ•œ ์Šคํ‚ค๋งˆ ๊ธฐ์ˆ ์ด๋ฏ€๋กœ ์กด์น˜ํ•œ๋‹ค. + ์ž„์˜ ์‚ญ์ œ ๊ธˆ์ง€ โ€” ์‚ญ์ œ ์‹œ 4๊ฐœ ์–ด๋Œ‘ํ„ฐ์— ํ•„๋“œ๋ช…์„ ๋‹ค์‹œ ํฉ๋ฟŒ๋ ค์•ผ ํ•œ๋‹ค.""" + raise NotImplementedError +``` + +๊ทผ๊ฑฐ ์—†๋Š” ๋ฏธ์‚ฌ์šฉ ์†์„ฑ์€ ๋‹ค์Œ ์ •๋ฆฌ ๋ผ์šด๋“œ์—์„œ "์‰ฌ์šด ์‚ญ์ œ ๋Œ€์ƒ"์œผ๋กœ ์˜ค์ธ๋ฉ๋‹ˆ๋‹ค โ€” C-4 ๊ฐ€ `_HERDR_SHIM_DIR_PATTERN` ์—์„œ ์ •ํ™•ํžˆ ๊ทธ ์‚ฌ๋ก€์˜€์Šต๋‹ˆ๋‹ค. + +--- + +## 7. ๋ฌธ์„œ ๋™๊ธฐํ™” + +### 7.1 `IMPROVEMENTS.md` โ€” 7๊ณณ + +| ํ–‰ | ํ˜„์žฌ | ๋ณ€๊ฒฝ ํ›„ | +|---|---|---| +| `:3` | ์ตœ์ข… ๊ฐฑ์‹ ์ผ `2026-08-17 (โ€ฆ, C-6 ์™„๋ฃŒ, 263/263)` | B-10 ์™„๋ฃŒ ๋ฐ 266/266 ๋ฐ˜์˜ | +| `:5` | ๋ฏธํ•ด๊ฒฐ **4๊ฑด** (์•„ํ‚ค 1, **์—ฃ์ง€ 3**, ์˜ค์ผ€ 0, ๋ ˆ๊ฑฐ์‹œ 0) | ๋ฏธํ•ด๊ฒฐ **3๊ฑด** (์•„ํ‚ค 1, **์—ฃ์ง€ 2**, ์˜ค์ผ€ 0, ๋ ˆ๊ฑฐ์‹œ 0) | +| `:6` | ์™„๋ฃŒ **21๊ฑด** | ์™„๋ฃŒ **22๊ฑด**, ๋ชฉ๋ก์— `B-10` ์ถ”๊ฐ€ | +| `:70` | `## 2. โ€ฆ (Edge-case Bugs โ€” 3๊ฑด)` | `โ€ฆ (Edge-case Bugs โ€” 2๊ฑด)` | +| `:79-80` | B-10 ํ•ญ๋ชฉ | **์‚ญ์ œ** (ยง5 ๋กœ ์ด๋™) | +| `:92` | `## 5. โ€ฆ (Completed Tasks โ€” 21๊ฑด)` | `โ€ฆ (Completed Tasks โ€” 22๊ฑด)` | +| `:241` | `\| **P3-2** \| **B-10** \| tier-3 ์‹ ์› ์บ์‹œ ์กด์น˜/์ œ๊ฑฐ ๊ฒฐ์ • + PyYAML ์˜์กด ์™„ํ™” \| ์ค‘ \| A-4 M2 \|` | `โ€ฆ tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ (Option A) **(โœ… ์™„๋ฃŒ โ€” ์ „์ฒด 266/266 PASS)** \|` | + +ยง5 ์‹ ๊ทœ ํ•ญ๋ชฉ: + +```markdown +### **B-10 (P3-2): `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ (Option A)** โ€” โœ… ์™„๋ฃŒ +- ์ €์žฅ์†Œ ์ „์ฒด์— `agent_identities` ์“ฐ๊ธฐ ์ฝ”๋“œ๊ฐ€ 0๊ฑด์ž„์„ ์žฌํ™•์ธํ•˜๊ณ (๋ผ์ด๋ธŒ `.db` ์ตœ์ƒ์œ„ ํ‚ค์—๋„ ๋ถ€์žฌ), + ๊ตฌ์กฐ์ ์œผ๋กœ ํžˆํŠธ ๋ถˆ๊ฐ€์˜€๋˜ ์ฝ๊ธฐ ๊ฒฝ๋กœ 3๊ณณ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค โ€” `workspace_uuid.py` tier-3 ํด๋ฐฑ(28์ค„), + `reconcile.sh` drift D ์ง„๋‹จ(35์ค„), `stop_session.sh` purge ์‹œ ์บ์‹œ ์†Œ๊ฑฐ(6์ค„), ๊ด€๋ จ ์ฃผ์„ 3๊ณณ. + UUID ํ•ด๊ฒฐ์€ tier-1(per-row own id) โ†’ tier-2(์–ด๋Œ‘ํ„ฐ `discover()`) 2๋‹จ๊ณ„๋กœ ๋‹จ์ˆœํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +- **PyYAML ์˜์กด โ€” ์‹คํ–‰ ๊ฒฝ๋กœ ๊ธฐ์ค€์œผ๋กœ ํ•ด์†Œ**: `verify_session.py::mam_orchestrator_uuids` ๊ฐ€ + `yaml` ์„ ํ•จ์ˆ˜ ์ง„์ž… ์ฆ‰์‹œ import ํ•˜๊ณ  ์žˆ์–ด(`:10`), tier-3 ์„ ์ง€์›Œ๋„ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๋Š” PyYAML ์„ + ์š”๊ตฌํ–ˆ์Šต๋‹ˆ๋‹ค. `state.py` ์˜ ๊ธฐ์กด ์„ ๋ก€๋Œ€๋กœ YAML ํด๋ฐฑ ๋ถ„๊ธฐ ์•ˆ์œผ๋กœ ์ด๋™์‹œ์ผœ ๊ต์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. +- **์ •์ •**: ์› ํ•ญ๋ชฉ์ด ์„œ์ˆ ํ–ˆ๋˜ "`lib.sh` ์˜ PyYAML ํ•˜๋“œ ์˜์กด" ์€ `load_state_json` ์ด `state.py` ๋กœ + ์ด๊ด€๋˜๋ฉฐ **์ด๋ฏธ ํ•ด์†Œ๋œ ์ƒํƒœ**์˜€์Šต๋‹ˆ๋‹ค. ํ•œํŽธ `atomic_yaml.py` ๋Š” ๋ชจ๋“ˆ ์กด์žฌ ์ด์œ ์ƒ ์•ž์œผ๋กœ๋„ + ์ตœ์ƒ๋‹จ์—์„œ import ํ•˜๋ฏ€๋กœ **์ €์žฅ์†Œ ์ฐจ์›์˜ PyYAML ์š”๊ตฌ์™€ ์„ค์น˜ ๊ฒŒ์ดํŠธ๋Š” ์œ ์ง€**๋ฉ๋‹ˆ๋‹ค. +- ํšŒ๊ท€ ๊ฐ€๋“œ 3์ข…์„ ์‹ ์„คํ•˜๊ณ  ๋ฎคํ…Œ์ด์…˜ 5์ข…(M1ยทM2ยทM3aยทM3bยทM4)์œผ๋กœ ๋ฐฉ์–ด๋ ฅ์„ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. +``` + +**์ฃผ์˜**: `:5` ์˜ ์—ฃ์ง€์ผ€์ด์Šค ์นด์šดํŠธ์™€ `:70` ยง2 ํ—ค๋”๋Š” **๋ฐ˜๋“œ์‹œ ํ•จ๊ป˜** ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. + +### 7.2 `VERSIONS.md` + +`### ๐Ÿš€ v2.0.0` changelog ์— `#### 7` ์ถ”๊ฐ€: + +```markdown +#### 7. `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ ๋ฐ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ PyYAML ํƒˆ์˜์กด (B-10 / Option A) +- ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ ๊ตฌ์กฐ์ ์œผ๋กœ ํžˆํŠธ ๋ถˆ๊ฐ€์˜€๋˜ tier-3 ํด๋ฐฑ๊ณผ ๋ถ€์† ์†Œ๋น„์ž + (`workspace_uuid.py`, `reconcile.sh` drift D, `stop_session.sh` ์บ์‹œ ์†Œ๊ฑฐ)๋ฅผ ์ „๋ฉด ์‚ญ์ œ. +- UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๋ฅผ **tier-1(per-row own id) โ†’ tier-2(์–ด๋Œ‘ํ„ฐ `discover()`)** 2๋‹จ๊ณ„๋กœ ๋‹จ์ˆœํ™”. +- `verify_session.py::mam_orchestrator_uuids` ์˜ ์ฆ‰์‹œ `yaml` import ๋ฅผ YAML ํด๋ฐฑ ๋ถ„๊ธฐ๋กœ ์ด๋™, + UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๊ฐ€ PyYAML ์—†์ด ์™„์ฃผํ•จ์„ ์‹คํ–‰ ๊ฐ€๋“œ๋กœ ๊ณ ์ • + (`atomic_yaml.py` ์˜ ์‹œ์Šคํ…œ PyYAML ์š”๊ตฌ๋Š” ์„ค๊ณ„์ƒ ์œ ์ง€). +- ํšŒ๊ท€ ๊ฐ€๋“œ 3์ข… ์‹ ์„ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ ๋ถ€ํ™œ ์ฐจ๋‹จ, `import yaml` AST ๊ฒ€์‚ฌ(์ง€์—ฐ import ํฌํ•จ), ์‹คํ–‰ ๊ฒฝ๋กœ ๊ฒ€์ฆ. +``` + +`:44` ์˜ A-4 ์ธํ„ฐํŽ˜์ด์Šค ๋‚˜์—ด์—์„œ `identity_cache_fields` ๋Š” ยง6 Option A ์— ๋”ฐ๋ผ **์œ ์ง€**ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 8. ๊ฒ€์ฆ ์ ˆ์ฐจ + +| # | ๋ช…๋ น / ํ™•์ธ | ๊ธฐ๋Œ€ | +|---|---|---| +| 1 | `bash -n` โ€” `lib.sh`, `reconcile.sh`, `stop_session.sh` | 3/3 OK (ํด๋ก  ์‹ค์ธก ์™„๋ฃŒ) | +| 2 | `python -c "import ast; ast.parse(open('workspace_uuid.py').read())"` | OK (ํด๋ก  ์‹ค์ธก ์™„๋ฃŒ) | +| 3 | `grep -rn "agent_identities" .agents/skills/` | ์ฃผ์„ ์™ธ **0๊ฑด** | +| 4 | `grep -rn "tier-3\|3-tier" .agents/skills/` | **0๊ฑด** | +| 5 | `grep -n sqlite3 lib_py/workspace_uuid.py` | **0๊ฑด** | +| 6 | `grep -n "yaml" lib_py/verify_session.py` | ํด๋ฐฑ ๋ถ„๊ธฐ ๋‚ด๋ถ€ 1๊ฑด๋งŒ | +| 7 | ๋ผ์ด๋ธŒ ์›Œํฌ์ŠคํŽ˜์ด์Šค์—์„œ `find_workspace_uuid claude` | ๋ณ€๊ฒฝ ์ „๊ณผ **๋™์ผ ์ถœ๋ ฅ** | +| 8 | `reconcile.sh` 1ํšŒ ์‹คํ–‰ ํ›„ `drifts` ํด๋ž˜์Šค ์ง‘ํ•ฉ | D ๋ฏธ์ถœํ˜„, A/B/C ์ •์ƒ | +| 9 | **๋ฎคํ…Œ์ด์…˜ M1ยทM2ยทM3aยทM3bยทM4** | ๊ฐ๊ฐ ํ•ด๋‹น ๊ฐ€๋“œ **FAIL** (ยง4.2 ์žฌํ˜„) | +| 10 | `pytest tests/ -q` | **266 passed** (263 ์‹ค์ธก + ๊ฐ€๋“œ 3๊ฑด) | +| 11 | `env -u PYTHONPATH pytest tests/test_tier1_unit.py -q` | ์ „๋ถ€ ํ†ต๊ณผ (ํ™˜๊ฒฝ ๋น„์˜์กด) | +| 12 | `IMPROVEMENTS.md` `:5` โ†” `:70` ๋Œ€์กฐ | ์—ฃ์ง€ ์นด์šดํŠธ ์ผ์น˜ | +| 13 | `IMPROVEMENTS.md` `:6` โ†” `:92` ๋Œ€์กฐ | ๋‘˜ ๋‹ค 22๊ฑด | + +7๋ฒˆ์ด **๋™์ž‘ ๋™์ผ์„ฑ ํ•ต์‹ฌ ๊ฒ€์ฆ**์ž…๋‹ˆ๋‹ค โ€” tier-3 ์ด ํžˆํŠธ ๋ถˆ๊ฐ€์˜€๋‹ค๋Š” ์ฃผ์žฅ์ด ๋งž๋‹ค๋ฉด ์ถœ๋ ฅ์ด ๋ฐ”๋€Œ์–ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. + +10๋ฒˆ์€ ์•ฝ 6๋ถ„ 30์ดˆ ์†Œ์š”๋ฉ๋‹ˆ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰ ๊ถŒ์žฅ. + +--- + +## 9. ๊ทœ๋ชจ ๋ฐ ๋ฆฌ์Šคํฌ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ | +|---|---| +| `lib_py/workspace_uuid.py` | โˆ’28์ค„, `sqlite3` import ์ œ๊ฑฐ | +| `lib_py/verify_session.py` | **๐Ÿ†• yaml import 1์ค„ ์ด๋™** | +| `reconcile.sh` | โˆ’35์ค„ (import ๋Š” **๋ณด์กด**) | +| `stop_session.sh` | โˆ’6์ค„ + ์ฃผ์„ 1๊ณณ | +| `lib.sh` | ์ฃผ์„ 2๊ณณ | +| `base.py` | `identity_cache_fields` ๊ทผ๊ฑฐ docstring | +| SKILL.md 3์ข… | drift D ํ–‰ยท์˜ˆ์‹œ 1์ค„ยทํ•ด๊ฒฐ ์ˆœ์„œ | +| `IMPROVEMENTS.md` / `VERSIONS.md` | ์นด์šดํŠธยทํ•ญ๋ชฉ ์ด๋™ + changelog | +| `tests/test_tier1_unit.py` | ๊ฐ€๋“œ 3๊ฑด | +| **ํ…Œ์ŠคํŠธ ์ด๊ณ„** | 263 (์‹ค์ธก) โ†’ **266** | + +| ๋ฆฌ์Šคํฌ | ํ‰๊ฐ€ | +|---|---| +| ๋™์ž‘ ํšŒ๊ท€ | **๋‚ฎ์Œ.** ์ œ๊ฑฐ ๋Œ€์ƒ ์ „๋ถ€ ์ƒ์‚ฐ์ž 0์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ํด๋ก  ์ „์ฒด ํšŒ๊ท€๋กœ ํ™•์ธ(ยง10) | +| ๋‹จ๊ณ„ 4 ๋ถ€์ž‘์šฉ | **๋‚ฎ์Œ.** import ์œ„์น˜๋งŒ ์ด๋™ํ•˜๋ฉฐ `yaml` ์‚ฌ์šฉ ์ง€์ ์€ ๊ทธ๋Œ€๋กœ. `state.py` ์— ๋™์ผ ์„ ๋ก€ ์กด์žฌ | +| ๋ ˆ๊ฑฐ์‹œ ์ƒํƒœ ํŒŒ์ผ | โš ๏ธ ๊ตฌ๋ฒ„์ „ `agent_identities` ๊ฐ€ ๋‚จ์€ `.db`/`.yaml` ์ด ์žˆ์–ด๋„ tier-1ยทtier-2 ๊ฐ€ ๋™์ผ UUID ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. tier-3 ์€ ์•ž ๋‘ ๋‹จ๊ณ„๊ฐ€ ๋ชจ๋‘ ์‹คํŒจํ•ด์•ผ ๋„๋‹ฌํ•˜๋˜ ๊ฒฝ๋กœ์ด๊ณ , **์Šคํ‚ค๋งˆ๋ฅผ ์ง€์šฐ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์ฝ๊ธฐ๋ฅผ ๋ฉˆ์ถ”๋Š” ๊ฒƒ**์ด๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ํŒŒ๊ดด ์—†์Œ | +| drift D ์ง„๋‹จ ์ƒ์‹ค | **์˜ํ–ฅ ์—†์Œ.** ์ƒ์‚ฐ์ž 0์ด๋ฏ€๋กœ ํ•œ ๋ฒˆ๋„ ๋ฐœํ™”ํ•œ ์  ์—†์Œ | +| `identity_cache_fields` ๊ณ ์•„ํ™” | ยง6 Option A + ๊ทผ๊ฑฐ docstring ์œผ๋กœ ์ฐจ๋‹จ | +| ๊ฐ€๋“œ ๋ฌด๋ ฅํ™” | ยง4.2 ๋ฎคํ…Œ์ด์…˜ 5์ข…์œผ๋กœ ์ฐจ๋‹จ | + +### ๊ถŒ์žฅ ์ปค๋ฐ‹ ๋ถ„ํ•  + +1. `refactor(uuid): drop the dead agent_identities tier-3 fallback (B-10)` โ€” ๋‹จ๊ณ„ 1~3 +2. `fix(verify): defer the yaml import so UUID resolution runs without PyYAML (B-10)` โ€” ๋‹จ๊ณ„ 4 +3. `test(b10): guard the read path, the yaml import, and the executed resolution path` โ€” ยง4 +4. `docs: sync comments, SKILL.md resolution order, IMPROVEMENTS.md and VERSIONS.md for B-10` โ€” ๋‹จ๊ณ„ 5~6 + ยง7 + +3๋ฒˆ์„ 1ยท2๋ฒˆ ๋’ค์— ๋‘๋ฉด ๊ฐ€๋“œ๊ฐ€ ์•ž ์ปค๋ฐ‹ ์—†์ด ์‹คํŒจํ•˜๊ณ  ํ•จ๊ป˜๋Š” ํ†ต๊ณผํ•จ์„ ์ปค๋ฐ‹ ์ˆœ์„œ๋กœ ์ฆ๋ช…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 2๋ฒˆ์„ ๋ถ„๋ฆฌํ•˜๋Š” ์ด์œ ๋Š” ์ด๊ฒƒ์ด **์ฝ๊ธฐ ๊ฒฝ๋กœ ์ œ๊ฑฐ์™€ ๋…๋ฆฝ๋œ ์ƒ์‚ฐ ์ฝ”๋“œ ์ˆ˜์ •**์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค โ€” ๋˜๋Œ๋ฆด ์ผ์ด ์ƒ๊ธฐ๋ฉด ๋”ฐ๋กœ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 10. ํ•œ๊ณ„ + +- ๋ณธ ๊ณ„ํš์€ Planner ์‚ฐ์ถœ๋ฌผ์ด๋ฉฐ **์ €์žฅ์†Œ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค**(์ž‘์—… ํŠธ๋ฆฌ ๊ณ„ํš ์ „ํ›„ clean). ๋ชจ๋“  ์‹ค์ธก์€ ๊ฒฉ๋ฆฌ ํด๋ก (`git clone --local --no-hardlinks`)์—์„œ ์ˆ˜ํ–‰ํ–ˆ๊ณ  ํด๋ก ์€ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค. +- ๋‹จ๊ณ„ 1~4 ๋ฅผ ํด๋ก ์— ์„ ์ ์šฉํ•ด ๊ตฌ๋ฌธ ๊ฒ€์‚ฌยท๊ฐ€๋“œยท๋ฎคํ…Œ์ด์…˜ยท์ „์ฒด ํšŒ๊ท€๋ฅผ ์‹ค์ธกํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹จ๊ณ„ 5~6(์ฃผ์„ยท๋ฌธ์„œ)์€ ์‹คํ–‰ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ฏ€๋กœ ์ ์šฉํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. +- **ํด๋ก  ์ „์ฒด ํšŒ๊ท€ ์‹ค์ธก ์™„๋ฃŒ**: ๋‹จ๊ณ„ 1~4 + ๊ฐ€๋“œ 3์ข…์„ ์ ์šฉํ•œ ํด๋ก ์—์„œ **266 passed in 408.18s (0:06:48)** โ€” ๊ธฐ์ค€ ๋ฒ ์ด์Šค๋ผ์ธ `7e21077` ์˜ 263 passed in 408.21s ๋Œ€๋น„ **์ •ํ™•ํžˆ ๊ฐ€๋“œ 3๊ฑด ์ˆœ์ฆ, ํšŒ๊ท€ 0๊ฑด**์ž…๋‹ˆ๋‹ค. ์ฆ‰ ยง8-10 ์˜ 266 ๋ชฉํ‘œ์น˜๋Š” ์˜ˆ์ธก์ด ์•„๋‹ˆ๋ผ ์‹ค์ธก๊ฐ’์ž…๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด๋Š” ํด๋ก  ์ธก์ •์ด๋ฏ€๋กœ ๊ตฌํ˜„ ํ›„ ์ €์žฅ์†Œ์—์„œ ์žฌํ™•์ธํ•˜์‹ญ์‹œ์˜ค. +- ์ œ์•ˆ ๊ฐ€๋“œ 3์ข…์˜ ํ•จ์ˆ˜ ์ˆ˜๊ฐ€ 3์ด๋ฏ€๋กœ 266 ์„ ๋ชฉํ‘œ์น˜๋กœ ์žก์•˜์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„ ์‹œ ํ•จ์ˆ˜๋ฅผ ๋ณ‘ํ•ฉํ•˜๋ฉด ์ˆ˜์น˜๋ฅผ ๊ทธ์— ๋งž์ถฐ ์กฐ์ •ํ•˜์‹ญ์‹œ์˜ค. +- ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์˜ ๋ ˆ๊ฑฐ์‹œ `.db`/`.yaml` ์— ์‹ค์ œ๋กœ `agent_identities` ๊ฐ€ ๋‚จ์•„ ์žˆ๋Š”์ง€๋Š” **์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค ํ•œ ๊ณณ๋งŒ** ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค(๋ถ€์žฌ). +- `verify_session.py` ์˜ `yaml` ์ง€์—ฐํ™”๋Š” `mam_orchestrator_uuids` ํ•œ ํ•จ์ˆ˜๋งŒ ๋Œ€์ƒ์œผ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ํŒŒ์ผ์˜ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋Š” ๊ฐ์‚ฌํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค(`grep` ์ƒ `yaml` ์ฐธ์กฐ๋Š” ์ด ํ•จ์ˆ˜์—๋งŒ ์กด์žฌ). diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-2f64681f.md b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-2f64681f.md new file mode 100644 index 0000000..cd62427 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-2f64681f.md @@ -0,0 +1,105 @@ +# Cross-Code Review Report: B-10 โ€” `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ (Option A) + +**Job ID**: `2f64681f` +**Reviewer**: cline +**Date**: 2026-08-17 +**Changeset**: 12 files, +99/-114 lines (git diff HEAD) + +--- + +## 1. Changeset Overview + +The B-10 backlog item (Option A) eliminates the dead `agent_identities` reading path and tier-3 fallback, removes PyYAML dependency from `workspace_uuid.py`, and simplifies UUID lookup to a 2-tier resolution (tier-1: per-row own id โ†’ tier-2: adapter `discover()`). + +### Files Changed (12 files) + +| File | Change | Lines | +|---|---|---| +| `lib.sh` | Comment updates: 3-tier โ†’ 2-tier description | +4/-9 | +| `lib_py/agents/base.py` | Docstring added to `identity_cache_fields` (retention rationale) | +4/-0 | +| `lib_py/verify_session.py` | `import yaml` moved from top-level to YAML fallback branch | +2/-1 | +| `lib_py/workspace_uuid.py` | Tier-3 fallback block removed (32 lines); `sqlite3` import removed | +1/-33 | +| `multi-agent-mux-monitor/SKILL.md` | "### D. Stale UUID" detailed section removed | +0/-10 | +| `multi-agent-mux-monitor/scripts/reconcile.sh` | Drift D detection code removed (37 lines) | +0/-37 | +| `multi-agent-mux-resume/SKILL.md` | UUID resolution order updated to 2-tier | +4/-7 | +| `multi-agent-mux-status/SKILL.md` | Drift class D table row removed | +0/-1 | +| `multi-agent-mux-stop/scripts/stop_session.sh` | Cache clearing code removed (6 lines); comment updated | +1/-7 | +| `IMPROVEMENTS.md` | B-10 moved from open to completed; counts updated | +12/-9 | +| `VERSIONS.md` | B-10 entry added under v2.0.0 item 7 | +7/-0 | +| `tests/test_tier1_unit.py` | 3 new regression tests (B-10 guards) | +64/-0 | + +--- + +## 2. Verification Results + +### 2.1 Lint (๋ฆฐํŠธ) โ€” โœ… PASS + +| Check | Method | Result | +|---|---|---| +| `bash -n lib.sh` | Syntax check | โœ… OK | +| `bash -n reconcile.sh` | Syntax check | โœ… OK | +| `bash -n stop_session.sh` | Syntax check | โœ… OK | +| `py_compile workspace_uuid.py` | Python compile check | โœ… OK | +| `py_compile verify_session.py` | Python compile check | โœ… OK | +| `py_compile base.py` | Python compile check | โœ… OK | +| `agent_identities` in production code | `grep -rn` across 4 target files | โœ… NO MATCHES (even in comments) | +| `import yaml` in workspace_uuid.py | `grep -n yaml` | โœ… NO MATCHES โ€” PyYAML dependency removed | +| `import yaml` in verify_session.py | `grep -n import yaml` | โœ… Only at line 50 (inside YAML fallback branch) | +| IMPROVEMENTS.md header counts | `grep` + `wc -l` | โœ… 3 open (1 arch + 2 edge), 22 completed | +| Section 2 item count | `sed` + `grep -c` | โœ… 2 items (B-13, B-9) โ€” B-10 removed | +| Section 5 item count | `sed` + `grep -c` | โœ… 22 entries (matches header list) | +| B-10 in completed list | `grep B-10` | โœ… In header line 6, section 5 detailed entry, update date | +| VERSIONS.md B-10 entry | `grep -n B-10` | โœ… Line 74, item 7 under v2.0.0 | +| 3 new B-10 tests | `grep -n 'def test_b10'` | โœ… All 3 present (lines 343, 364, 382) | +| `identity_cache_fields` retention | `grep -rn` in adapters | โœ… Retained in base.py + 4 adapters with docstring | + +### 2.2 Operability (๋™์ž‘์„ฑ) โ€” โœ… PASS + +| Check | Method | Result | +|---|---|---| +| B-10 regression tests | `pytest -k b10` | โœ… 3/3 PASSED (0.04s) | +| `test_b10_no_agent_identities_reader_in_production` | Unit test | โœ… PASSED โ€” guards against agent_identities read path resurrection | +| `test_b10_workspace_uuid_has_no_yaml_import` | AST analysis | โœ… PASSED โ€” guards against `import yaml` reintroduction (including lazy) | +| `test_b10_find_workspace_uuid_runs_without_pyyaml` | Subprocess stub | โœ… PASSED โ€” UUID resolution path completes without PyYAML | +| Full regression suite | `pytest tests/ -v` | โœ… **266/266 PASS (100%) in 411.09s** | + +### 2.3 Loss (์œ ์‹ค) โ€” โœ… PASS + +| Check | Method | Result | +|---|---|---| +| Tier-3 fallback removed from workspace_uuid.py | `git diff` | โœ… 32-line block removed; `sqlite3` import removed | +| Drift D removed from reconcile.sh | `git diff` | โœ… 37-line block removed (claude/agy/hermes/cline stale UUID checks) | +| Cache clearing removed from stop_session.sh | `git diff` | โœ… 6-line block removed (agent_identities purge on --purge-conversation) | +| PyYAML removed from workspace_uuid.py | `grep yaml` | โœ… No yaml references at all | +| PyYAML moved to lazy import in verify_session.py | `git diff` | โœ… `import yaml` now inside YAML fallback `try` block (line 50) | +| UUID resolution simplified to 2-tier | Code inspection | โœ… tier-1 (per-row own id) โ†’ tier-2 (adapter discover()) โ†’ print('') | +| `identity_cache_fields` retained intentionally | `grep` + docstring | โœ… Retained with docstring explaining future cache write path need | +| SKILL.md docs updated (resume, monitor, status) | `git diff` | โœ… All 3 docs updated to reflect 2-tier resolution and drift D removal | +| Comments updated (lib.sh, stop_session.sh) | `git diff` | โœ… "3-tier" โ†’ "2-tier", tier-3 references removed | +| No production code reads agent_identities | Regression test | โœ… `test_b10_no_agent_identities_reader_in_production` guards this | + +--- + +## 3. Minor Non-Blocking Observations + +1. **Monitor SKILL.md line 175**: The "Drift responses" summary list still contains "- D. Stale UUID: report only, no YAML change" even though the detailed "### D. Stale UUID" section and the reconcile.sh drift D implementation were both removed. This summary reference was outside the diff hunk and was not cleaned up. **Non-blocking** โ€” the implementation is correctly removed; only a documentation summary line is stale. Consider removing line 175 in a future cleanup. + +2. **`identity_cache_fields` retention**: The `identity_cache_fields` property is retained in `base.py` and all 4 adapters (claude, agy, hermes, cline) with a Korean docstring explaining that while the read path has zero production consumers post-B-10, it remains the sole schema description needed when a cache write path is introduced. This is an intentional, documented design decision โ€” not dead code to remove. + +3. **`import yaml` in verify_session.py**: The `yaml` import is now inside a conditional branch (YAML fallback at line 50), only executed when `.db` doesn't contain `orchestrator_uuids` and the YAML file exists. This follows the existing `state.py` precedent for lazy YAML imports. The UUID resolution path can complete without PyYAML when the `.db` file has the data, as verified by `test_b10_find_workspace_uuid_runs_without_pyyaml`. + +--- + +## 4. Verdict + +The B-10 backlog item has been correctly resolved via Option A (complete elimination): + +- **`agent_identities` read paths removed** from all 3 production locations: `workspace_uuid.py` (tier-3 fallback, 32 lines), `reconcile.sh` (drift D detection, 37 lines), `stop_session.sh` (cache clearing on purge, 6 lines). +- **PyYAML dependency removed** from `workspace_uuid.py` (no `import yaml` at all) and deferred to a lazy conditional import in `verify_session.py` (YAML fallback branch only). +- **UUID resolution simplified** to a clean 2-tier model: tier-1 (per-row own id from `herdr_sessions[]`) โ†’ tier-2 (adapter `discover()` on-disk scan) โ†’ empty result. +- **Regression guards** (3 new tests) protect against resurrection of the `agent_identities` read path, reintroduction of `import yaml` in `workspace_uuid.py` (including lazy imports via AST analysis), and runtime PyYAML dependency in the resolution path. +- **Documentation updated** consistently across 6 files: `lib.sh` comments, 3 SKILL.md files, `IMPROVEMENTS.md` (B-10 moved to completed, counts updated to 3 open / 22 completed), `VERSIONS.md` (entry 7 under v2.0.0). +- **Full regression suite passes**: 266/266 PASS (100%) in 411.09s. +- The `identity_cache_fields` property is intentionally retained with documentation for future cache write path use. + +[VERDICT: PASS] \ No newline at end of file diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 77bfded..dd316dd 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1298,15 +1298,10 @@ verify_tui_viewport() { # --------------------------------------------------------------------------- # find_workspace_uuid # -# Workspace-SCOPED resolution of the resume UUID (P0-C). It NEVER returns a -# global agent_identities id unless that id's project_cwd matches THIS -# workspace. Resolution order: +# Workspace-SCOPED resolution of the resume UUID (P0-C). Resolution order: # 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id # (claude_session_id_own / agy_conversation_id_own) -# 2) on-disk scan scoped to this workspace -# (claude: ~/.claude/projects//*.jsonl ; agy: last_conversations.json[cwd]) -# 3) agent_identities cache in d (primary) or $YAML_PATH (fallback), ONLY when its project_cwd == this workspace. -# (Note: DB is authority, YAML is mirror; tier-3 never prioritizes mirror over DB) +# 2) on-disk scan scoped to this workspace, via the agent adapter's discover() # Prints the UUID on stdout (empty line if none). Always exits 0. # --------------------------------------------------------------------------- find_workspace_uuid() { @@ -1323,8 +1318,8 @@ PYEOF # # Thin wrapper over find_workspace_uuid: resolves THIS workspace's conversation # id (claude jsonl sessionId / agy db uuid) and prints it on stdout (empty line -# if none). find_workspace_uuid is already a workspace-scoped, 3-tier, race-free -# resolver (per-row own id -> workspace-scoped disk scan -> cwd-matched cache), +# if none). find_workspace_uuid is already a workspace-scoped, 2-tier, race-free +# resolver (per-row own id -> workspace-scoped disk scan), # so recording its result into the row before kill guarantees tier-1 on the next # resume. Pass session_name to prefer that specific row's recorded id. Always exits 0. # --------------------------------------------------------------------------- diff --git a/.agents/skills/lib_py/agents/base.py b/.agents/skills/lib_py/agents/base.py index 67fe791..27e2383 100644 --- a/.agents/skills/lib_py/agents/base.py +++ b/.agents/skills/lib_py/agents/base.py @@ -55,6 +55,10 @@ class BaseAgentAdapter: @property def identity_cache_fields(self) -> tuple: + """agent_identities ์บ์‹œ์˜ ์—์ด์ „ํŠธ๋ณ„ ํ•„๋“œ๋ช…. + B-10(Option A)๋กœ ์บ์‹œ ์ฝ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์ œ๊ฑฐ๋˜์–ด ํ˜„์žฌ ์ƒ์‚ฐ ์†Œ๋น„์ž๋Š” 0๊ฑด์ด์ง€๋งŒ, + ์บ์‹œ ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ๋„์ž…๋˜๋ฉด ์ฆ‰์‹œ ํ•„์š”ํ•œ ์œ ์ผํ•œ ์Šคํ‚ค๋งˆ ๊ธฐ์ˆ ์ด๋ฏ€๋กœ ์กด์น˜ํ•œ๋‹ค. + ์ž„์˜ ์‚ญ์ œ ๊ธˆ์ง€ โ€” ์‚ญ์ œ ์‹œ 4๊ฐœ ์–ด๋Œ‘ํ„ฐ์— ํ•„๋“œ๋ช…์„ ๋‹ค์‹œ ํฉ๋ฟŒ๋ ค์•ผ ํ•œ๋‹ค.""" raise NotImplementedError @property diff --git a/.agents/skills/lib_py/verify_session.py b/.agents/skills/lib_py/verify_session.py index 82e99dd..f5cbed2 100644 --- a/.agents/skills/lib_py/verify_session.py +++ b/.agents/skills/lib_py/verify_session.py @@ -7,7 +7,7 @@ def mam_orchestrator_uuids(): global _MAM_ORC_CACHE if _MAM_ORC_CACHE is not None: return _MAM_ORC_CACHE - import os, sys, json, sqlite3, yaml + import os, sys, json, sqlite3 override = os.environ.get("MAM_ORCHESTRATOR_UUIDS") if override is not None: if not override.strip(): @@ -47,6 +47,7 @@ def mam_orchestrator_uuids(): pass if (d_obj is None or "orchestrator_uuids" not in d_obj) and os.path.exists(yaml_p): try: + import yaml with open(yaml_p) as f: d_obj = yaml.safe_load(f) or {} except Exception: diff --git a/.agents/skills/lib_py/workspace_uuid.py b/.agents/skills/lib_py/workspace_uuid.py index b256acb..d50ebd8 100644 --- a/.agents/skills/lib_py/workspace_uuid.py +++ b/.agents/skills/lib_py/workspace_uuid.py @@ -1,7 +1,7 @@ # workspace_uuid.py โ€” workspace UUID discovery logic # Extracted from lib.sh find_workspace_uuid PYEOF block -import os, sys, json, sqlite3 +import os, sys, json from lib_py.verify_session import verify_session_uuid, mam_orchestrator_uuids, mam_row_own_uuid OWN_KEY = { @@ -80,38 +80,6 @@ def find_workspace_uuid_main(): for cand in adapter.discover(ctx): emit(cand) - ai = d.get('agent_identities') if isinstance(d, dict) else None - if not isinstance(ai, dict) or not ai: - ai = {} - try: - yaml_path = os.environ['YAML_PATH'] - db_path = os.path.splitext(yaml_path)[0] + '.db' - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - conn.execute('PRAGMA busy_timeout = 60000') - try: - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - ai = json.loads(row[0]).get('agent_identities') or {} - except sqlite3.OperationalError: - pass - conn.close() - elif os.path.exists(yaml_path): - import yaml - with open(yaml_path) as f: - _ydoc = yaml.safe_load(f) or {} - ai = _ydoc.get('agent_identities') or {} - except Exception as e: - print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr) - if not isinstance(ai, dict): - ai = {} - - ai_agent = ai.get(agent) or {} - if ai_agent.get('project_cwd') == ws: - cand = ai_agent.get('session_id') or ai_agent.get('conversation_id') - if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"): - emit(cand) - print('') if __name__ == '__main__': diff --git a/.agents/skills/multi-agent-mux-monitor/SKILL.md b/.agents/skills/multi-agent-mux-monitor/SKILL.md index f03a7bc..1c10a02 100644 --- a/.agents/skills/multi-agent-mux-monitor/SKILL.md +++ b/.agents/skills/multi-agent-mux-monitor/SKILL.md @@ -137,16 +137,6 @@ YAML: no such session ``` - **C-ambiguous. Multiple candidates detected**: If multiple candidate transcripts match an unassigned session, the monitor avoids random pinning, reports `C-ambiguous`, and sets `last_visible_status: "ambiguous: N candidates"`. -### D. Stale UUID (artifact gone) - -``` -YAML: agent_identities.claude.session_id=87dc548e-... -disk: ~/.claude/projects/.../87dc548e-...jsonl: missing - โ†’ report it, but DO NOT delete from YAML - (the user may have moved the file or the disk may be temporarily unavailable; - only `--purge-conversation` should remove the id) -``` - ## Pitfalls - **Don't expect `--once` to stay alive** โ€” it does a single pass and exits. Use `--subscribe` for continuous monitoring. diff --git a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh index ee167ef..171086f 100755 --- a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh +++ b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh @@ -790,43 +790,6 @@ for s in d.get('herdr_sessions', []): else: _pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=True) -# === drift D: stale UUID (cache ์˜ artifact ๊ฐ€ ์‚ฌ๋ผ์ง) โ€” ๋ณด๊ณ ๋งŒ, ๋ณ€๊ฒฝ ์—†์Œ === -ai = d.get('agent_identities', {}) or {} -cl = (ai.get('claude') or {}) -if cl.get('session_id'): - sid = cl['session_id'] - if not glob.glob(f"{claude_project_dir}/*/{sid}.jsonl"): - drifts.append({'class': 'D', 'name': '(claude identity cache)', - 'msg': f"stale UUID in agent_identities.claude.session_id: {sid} (jsonl missing)"}) -ag = (ai.get('agy') or {}) -if ag.get('conversation_id'): - cid = ag['conversation_id'] - if not os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"): - drifts.append({'class': 'D', 'name': '(agy identity cache)', - 'msg': f"stale UUID in agent_identities.agy.conversation_id: {cid} (.db missing)"}) -hr = (ai.get('hermes') or {}) -if hr.get('session_id'): - sid = hr['session_id'] - hdb = f"{home}/.hermes/state.db" - has_session = False - if os.path.exists(hdb): - try: - conn = sqlite3.connect(hdb) - r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (sid,)).fetchone() - conn.close() - has_session = r is not None - except Exception: - pass - if not has_session: - drifts.append({'class': 'D', 'name': '(hermes identity cache)', - 'msg': f"stale UUID in agent_identities.hermes.session_id: {sid} (session missing from db)"}) -cn = (ai.get('cline') or {}) -if cn.get('session_id'): - sid = cn['session_id'] - if not os.path.exists(f"{home}/.cline/data/sessions/{sid}/{sid}.json"): - drifts.append({'class': 'D', 'name': '(cline identity cache)', - 'msg': f"stale UUID in agent_identities.cline.session_id: {sid} (session file missing)"}) - result = { 'timestamp': now_iso, 'yaml_path': yaml_path, diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 836cae0..ad807d9 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -47,15 +47,12 @@ ideal resume path: ## UUID resolution order -`agent-sessions.yaml` is the *primary* source. The skill reads in this order: +`agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order: -1. **`agent-sessions.yaml` โ†’ `agent_identities..session_id` (claude) / `conversation_id` (agy)** โ€” explicit saved value -2. **`agent-sessions.yaml` โ†’ `agent_identities..session_jsonl` (claude) / `conversation_db` (agy)** โ€” the on-disk artifact -3. **Fallback: scan disk for the workspace's most recent conversation** (Note: `CLAUDE_PROJECT_DIR` overrides the default `~/.claude/projects/` path, and `HOME_DIR` overrides the `~` path) โ€” - - claude: `ls -t $CLAUDE_PROJECT_DIR//*.jsonl | head -1` and parse the `sessionId` from the first line - - agy: `jq -r '.""' $HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json` +1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `cline_conversation_id_own`) โ€” explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free). +2. **Workspace-scoped on-disk scan** (adapter `discover()`) -If all three are empty โ†’ the workspace has no conversation yet. Fall back to `multi-agent-mux-create`. +If both are empty โ†’ the workspace has no conversation yet. Fall back to `multi-agent-mux-create`. ## Workflow diff --git a/.agents/skills/multi-agent-mux-status/SKILL.md b/.agents/skills/multi-agent-mux-status/SKILL.md index 989251f..c619039 100644 --- a/.agents/skills/multi-agent-mux-status/SKILL.md +++ b/.agents/skills/multi-agent-mux-status/SKILL.md @@ -105,7 +105,6 @@ lab-paper-pdf2md-creator-claude default running alive clau | `A` | YAML `running`, herdr dead | session died without going through `multi-agent-mux-stop`. *Could* auto-terminate but won't โ€” that's `multi-agent-mux-monitor`'s job. | | `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or multi-agent-mux-stop to clean up." | | `C` | YAML has `claude_session_id_own: null` AND a new *.jsonl exists | new session id materialized; suggest: "run multi-agent-mux-resume or reconcile to register it." | -| `D` | YAML has UUID in `agent_identities`, but the on-disk artifact is gone | stale UUID; user should `multi-agent-mux-stop --purge-conversation` to clean up. | ## Pitfalls diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index f19fb1d..27aec40 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -164,7 +164,7 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then fi # ์บก์ฒ˜: kill ์ง์ „์— conversation id ๋ฅผ ํ•ด๊ฒฐ (process/jsonl ์ด ์•„์ง ์‚ด์•„์žˆ์„ ๋•Œ). -# find_workspace_uuid ๊ฐ€ tier-1(row) -> tier-2(workspace-scoped disk scan) -> tier-3(cache) +# find_workspace_uuid ๊ฐ€ tier-1(row) -> tier-2(workspace-scoped disk scan) # ๋ฅผ ์•Œ์•„์„œ ์‹œ๋„ํ•˜๋ฏ€๋กœ herdr ์ƒ์‚ฌ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋™์ž‘. CAPTURED_UUID="" if [ "$CAPTURE_ID" = "1" ] && [ -n "$TARGET_CWD" ]; then @@ -284,12 +284,6 @@ if purge and purge_uuid: for item in adapter.purge_artifacts(purge_uuid, ctx): print(f"purged: {item}", flush=True) target[adapter.own_key] = None - # agent_identities ๋Š” cache โ€” ์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒƒ์ผ ๋•Œ๋งŒ ๋น„์šด๋‹ค - ai = (d.get('agent_identities') or {}).get(agent) or {} - if ai.get('project_cwd') == ws: - if adapter and (ai.get('session_id') == purge_uuid or ai.get('conversation_id') == purge_uuid): - for field in adapter.identity_cache_fields: - ai[field] = None elif purge and not purge_uuid: print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True) diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 3ff1263..8f348f3 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -1,9 +1,9 @@ # ๐Ÿ› ๏ธ Multi-Agent Mux ์ข…ํ•ฉ ๊ฐœ์„  ๋ฐ ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ ๋ฐฑ๋กœ๊ทธ (`IMPROVEMENTS.md`) -- **์ตœ์ข… ๊ฐฑ์‹ ์ผ**: 2026-08-17 (B-5 macOS NFS ๊ฐ์ง€ df -P ํด๋ฐฑ ๊ฒ€์ฆ ๋ฐ ์ข…๊ฒฐ ์™„๋ฃŒ, C-6 ์™„๋ฃŒ, ์ „์ฒด 263/263 ํšŒ๊ท€ ํ†ต๊ณผ ๋ฐ˜์˜) +- **์ตœ์ข… ๊ฐฑ์‹ ์ผ**: 2026-08-17 (B-10/P3-2 agent_identities tier-3 ์‹ ์› ์บ์‹œ ์ œ๊ฑฐ ๋ฐ PyYAML ํƒˆ์˜์กด ์™„๋ฃŒ, B-5 ์ข…๊ฒฐ, C-6 ์™„๋ฃŒ, ์ „์ฒด 266/266 ํšŒ๊ท€ ํ†ต๊ณผ ๋ฐ˜์˜) - **ํ†ตํ•ฉ ๊ด€๋ฆฌ ๋Œ€์ƒ**: ๊ธฐ์กด `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` -- **์ด ์ถ”์  ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ**: **4๊ฑด** (์•„ํ‚คํ…์ฒ˜ 1๊ฑด, ์—ฃ์ง€์ผ€์ด์Šค 3๊ฑด, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ 0๊ฑด, ๋ ˆ๊ฑฐ์‹œ ์ž”์žฌ 0๊ฑด) -- **์™„๋ฃŒ๋œ ๊ณผ์ œ**: **21๊ฑด** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, 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) +- **์ด ์ถ”์  ๋ฏธํ•ด๊ฒฐ ๊ณผ์ œ**: **3๊ฑด** (์•„ํ‚คํ…์ฒ˜ 1๊ฑด, ์—ฃ์ง€์ผ€์ด์Šค 2๊ฑด, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ 0๊ฑด, ๋ ˆ๊ฑฐ์‹œ ์ž”์žฌ 0๊ฑด) +- **์™„๋ฃŒ๋œ ๊ณผ์ œ**: **22๊ฑด** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-10, 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,7 +67,7 @@ --- -## 2. ๐ŸŸ  ์—ฃ์ง€ ์ผ€์ด์Šค ๋ฐ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ (Edge-case Bugs โ€” 3๊ฑด) +## 2. ๐ŸŸ  ์—ฃ์ง€ ์ผ€์ด์Šค ๋ฐ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ (Edge-case Bugs โ€” 2๊ฑด) ### **B-13: ์…€ํ”„ ํ˜ธ์ŠคํŒ… ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ๋ฃจํ”„ ์ค‘ ํ„ด ๊ฐ„ ์Šคํ‚ฌ ์˜ค์—ผ (In-Flight Tooling Mutation)** โ€” ๐ŸŸก **Stage 2 ๋ถ„๋ฆฌ ๊ณผ์ œ** - `/multi-agent-mux-loop` ๊ฐ€ `multi-agent-mux` ์ž์ฒด๋ฅผ ์ˆ˜์ •/๋ฆฌํŒฉํ„ฐ๋งํ•  ๋•Œ, Turn 1์—์„œ Worker๊ฐ€ `.agents/skills/...` ๋ฅผ ์ˆ˜์ •(๋ฌธ๋ฒ• ์˜ค๋ฅ˜๋‚˜ ๋ฏธ์™„์„ฑ ์ฝ”๋“œ ํฌํ•จ)ํ•˜๋ฉด ํ›„์† Turn 2์˜ Reviewer ์žก ์ œ์ถœ ์‹œ ๋ž˜ํผ๊ฐ€ ๋น„์ •์ƒ ์ข…๋ฃŒ๋˜์–ด ์ž๊ฐ€ ์น˜์œ (Corrective/Rebuttal) ๋‹จ๊ณ„๋กœ ์ง„์ž…ํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋ฃจํ”„๊ฐ€ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๋Š” ์œ„ํ—˜์ž…๋‹ˆ๋‹ค. @@ -76,9 +76,6 @@ ### **B-9: `LOGS_DIR` import ์‹œ์  cwd ๊ณ ์ •** - `mqtt_common.py` ๋ชจ๋“ˆ ๋กœ๋“œ ์‹œ์ ์˜ cwd๋กœ ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฒฝ๋กœ๊ฐ€ 1ํšŒ ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค. -### **B-10: `agent_identities` ์“ฐ๊ธฐ ๊ฒฝ๋กœ ๋ถ€์žฌ ๋ฐ PyYAML ์˜์กด์„ฑ** -- ์ €์žฅ์†Œ ์ „์ฒด์— `agent_identities` ๋ฅผ ์ƒ์„ฑยท๊ฐฑ์‹ ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ 0๊ฑด์ด๋ฉฐ, `lib.sh` ์˜ PyYAML ํ•˜๋“œ ์˜์กด์„ฑ์œผ๋กœ ์ธํ•ด `.db` ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ์—๋„ PyYAML ๋ถ€์žฌ ์‹œ ์ƒํƒœ ์กฐํšŒ๊ฐ€ ๋ฌด๋ ฅํ™”๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. - --- ## 3. ๐ŸŸก ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ์ตœ์ ํ™” ๊ณผ์ œ (Orchestration Optimizations โ€” 0๊ฑด โ€” ์ „์› ์™„๋ฃŒ) @@ -89,7 +86,13 @@ --- -## 5. ๐ŸŽ‰ ์™„๋ฃŒ๋œ ๊ณผ์ œ (Completed Tasks โ€” 21๊ฑด) +## 5. ๐ŸŽ‰ ์™„๋ฃŒ๋œ ๊ณผ์ œ (Completed Tasks โ€” 22๊ฑด) + +### **B-10 (P3-2): `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ (Option A)** โ€” โœ… ์™„๋ฃŒ +- ์ €์žฅ์†Œ ์ „์ฒด์— `agent_identities` ์“ฐ๊ธฐ ์ฝ”๋“œ๊ฐ€ 0๊ฑด์ž„์„ ์žฌํ™•์ธํ•˜๊ณ (๋ผ์ด๋ธŒ `.db` ์ตœ์ƒ์œ„ ํ‚ค์—๋„ ๋ถ€์žฌ), ๊ตฌ์กฐ์ ์œผ๋กœ ํžˆํŠธ ๋ถˆ๊ฐ€์˜€๋˜ ์ฝ๊ธฐ ๊ฒฝ๋กœ 3๊ณณ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค โ€” `workspace_uuid.py` tier-3 ํด๋ฐฑ(28์ค„), `reconcile.sh` drift D ์ง„๋‹จ(35์ค„), `stop_session.sh` purge ์‹œ ์บ์‹œ ์†Œ๊ฑฐ(6์ค„), ๊ด€๋ จ ์ฃผ์„ 3๊ณณ. UUID ํ•ด๊ฒฐ์€ tier-1(per-row own id) โ†’ tier-2(์–ด๋Œ‘ํ„ฐ `discover()`) 2๋‹จ๊ณ„๋กœ ๋‹จ์ˆœํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +- **PyYAML ์˜์กด โ€” ์‹คํ–‰ ๊ฒฝ๋กœ ๊ธฐ์ค€์œผ๋กœ ํ•ด์†Œ**: `verify_session.py::mam_orchestrator_uuids` ๊ฐ€ `yaml` ์„ ํ•จ์ˆ˜ ์ง„์ž… ์ฆ‰์‹œ import ํ•˜๊ณ  ์žˆ์–ด(`:10`), tier-3 ์„ ์ง€์›Œ๋„ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๋Š” PyYAML ์„ ์š”๊ตฌํ–ˆ์Šต๋‹ˆ๋‹ค. `state.py` ์˜ ๊ธฐ์กด ์„ ๋ก€๋Œ€๋กœ YAML ํด๋ฐฑ ๋ถ„๊ธฐ ์•ˆ์œผ๋กœ ์ด๋™์‹œ์ผœ ๊ต์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. +- **์ •์ •**: ์› ํ•ญ๋ชฉ์ด ์„œ์ˆ ํ–ˆ๋˜ "`lib.sh` ์˜ PyYAML ํ•˜๋“œ ์˜์กด" ์€ `load_state_json` ์ด `state.py` ๋กœ ์ด๊ด€๋˜๋ฉฐ **์ด๋ฏธ ํ•ด์†Œ๋œ ์ƒํƒœ**์˜€์Šต๋‹ˆ๋‹ค. ํ•œํŽธ `atomic_yaml.py` ๋Š” ๋ชจ๋“ˆ ์กด์žฌ ์ด์œ ์ƒ ์•ž์œผ๋กœ๋„ ์ตœ์ƒ๋‹จ์—์„œ import ํ•˜๋ฏ€๋กœ **์ €์žฅ์†Œ ์ฐจ์›์˜ PyYAML ์š”๊ตฌ์™€ ์„ค์น˜ ๊ฒŒ์ดํŠธ๋Š” ์œ ์ง€**๋ฉ๋‹ˆ๋‹ค. +- ํšŒ๊ท€ ๊ฐ€๋“œ 3์ข…(`test_b10_no_agent_identities_reader_in_production`, `test_b10_workspace_uuid_has_no_yaml_import`, `test_b10_find_workspace_uuid_runs_without_pyyaml`)์„ ์‹ ์„คํ•˜๊ณ  ๋ฎคํ…Œ์ด์…˜ 5์ข…(M1ยทM2ยทM3aยทM3bยทM4)์œผ๋กœ ๋ฐฉ์–ด๋ ฅ์„ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. ### **B-5: macOS NFS ๊ฐ์ง€ `df -P` ํด๋ฐฑ ๊ฒ€์ฆ ๋ฐ ์ข…๊ฒฐ** โ€” โœ… ์™„๋ฃŒ (์ข…๊ฒฐ) - `_check_is_nfs`(`lib.sh:1181-1192`)์—์„œ macOS/BSD ํ™˜๊ฒฝ ์‹œ GNU ์ „์šฉ `df --output=target` ์‹คํŒจ(`rc=64`)์— ๋Œ€๋น„ํ•œ `df -P "$f" | tail -1 | awk '{print $6}'` POSIX ํด๋ฐฑ์ด ์ •์ƒ ๋™์ž‘ํ•จ์„ ์‹ค์ธก ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(`test_stop_check_is_nfs_local`)๋กœ ๊ฒ€์ฆ ์™„๋ฃŒํ•˜์—ฌ ์ข…๊ฒฐ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. @@ -238,7 +241,7 @@ | **P2-2** | **C-3a + C-4** | ๋นˆ ์Šคํ… 4์ข… + ๊ณตํ—ˆํ•œ ํ…Œ์ŠคํŠธ 4๊ฑด + ์ฃฝ์€ ์‹ฌ๋ณผ 3์ข… ์ œ๊ฑฐ ๋ฐ `--isolate` no-op ํšŒ๊ท€ ๊ฐ€๋“œ ์‹ ์„ค **(โœ… ์™„๋ฃŒ โ€” tests/test_tier1_unit.py + test_tier2_component.py 256/256 PASS)** | ์†Œ | โ€” | | **P2-3** | **C-6** | `stop_session.sh` ํ—ค๋”/๋„์›€๋ง/์ฃผ์„/MESSAGING.md ์ •๋ฆฌ ๋ฐ ํšŒ๊ท€ ๊ฐ€๋“œ ์‹ ์„ค **(โœ… ์™„๋ฃŒ โ€” tests/test_tier2_component.py ๊ฐ€๋“œ ์‹ ์„ค, ์ „์ฒด 263/263 PASS)** | ๊ทน์†Œ | โ€” | | **P3-1** | **A-4 M2~M7** | ์–ด๋Œ‘ํ„ฐ ๋ณธ์ด๊ด€ ๋ฐ CLI facts ๋ธŒ๋ฆฌ์ง€/์„œ๋ธŒ์ปค๋งจ๋“œ ๊ตฌ์ถ• **(โœ… ์™„๋ฃŒ โ€” tests/test_a4_adapter_contract.py 9/9 PASS, ์ „์ฒด 259/259 PASS)** | ๋Œ€ | P1-1 | -| **P3-2** | **B-10** | tier-3 ์‹ ์› ์บ์‹œ ์กด์น˜/์ œ๊ฑฐ ๊ฒฐ์ • + PyYAML ์˜์กด ์™„ํ™” | ์ค‘ | A-4 M2 | +| **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 | | **P4-1** | **B-9** | ๊ธฐ๋ณธ๊ฐ’ ํ•œ์ •. `logs_dir` ์ธ์žยท`DELEGATE_JOB_LOGS_DIR` ๋‘ ๊ฐ€์ง€ ํšŒํ”ผ ์ˆ˜๋‹จ ์กด์žฌ | ๊ทน์†Œ | โ€” | | **P5-1** | **A-2** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ๋ฐ HMAC ๊ฒ€์ฆ ๋ณด์™„ (๐Ÿ“Œ *์‚ฌ์šฉ์ž ์ง€์นจ: ์ฐจํ›„ ์ „์šฉ MQTT ๋ธŒ๋กœ์ปค ์„œ๋น™ ํ™˜๊ฒฝ ๊ตฌ์ถ• ์‹œ์ ์— ์ง„ํ–‰*) | ์ค‘ (3ํŒŒ์ผ) | ์ „์šฉ ๋ธŒ๋กœ์ปค | diff --git a/VERSIONS.md b/VERSIONS.md index b756135..78d5878 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -71,6 +71,13 @@ #### 6. macOS NFS ๊ฐ์ง€ `df -P` ํด๋ฐฑ ๊ฒ€์ฆ ๋ฐ ์ข…๊ฒฐ (B-5) - `_check_is_nfs`(`lib.sh`)์˜ macOS/BSD ํ™˜๊ฒฝ ๋‚ด GNU ์ „์šฉ `df --output` ๊ตฌ๋ฌธ ์˜ค๋ฅ˜ ์‹œ POSIX `df -P` ํด๋ฐฑ ๋™์ž‘์„ ์‹ค์ธก ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(`test_stop_check_is_nfs_local`)๋กœ ๊ฒ€์ฆ ์™„๋ฃŒํ•˜์—ฌ B-5 ์ด์Šˆ๋ฅผ ์ •์‹ ์ข…๊ฒฐ. +#### 7. `agent_identities` tier-3 ์‹ ์› ์บ์‹œ ์™„์ „ ์ œ๊ฑฐ ๋ฐ UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ PyYAML ํƒˆ์˜์กด (B-10 / Option A) +- ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ ๊ตฌ์กฐ์ ์œผ๋กœ ํžˆํŠธ ๋ถˆ๊ฐ€์˜€๋˜ tier-3 ํด๋ฐฑ๊ณผ ๋ถ€์† ์†Œ๋น„์ž(`workspace_uuid.py`, `reconcile.sh` drift D, `stop_session.sh` ์บ์‹œ ์†Œ๊ฑฐ)๋ฅผ ์ „๋ฉด ์‚ญ์ œ. +- UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๋ฅผ **tier-1(per-row own id) โ†’ tier-2(์–ด๋Œ‘ํ„ฐ `discover()`)** 2๋‹จ๊ณ„๋กœ ๋‹จ์ˆœํ™”. +- `verify_session.py::mam_orchestrator_uuids` ์˜ ์ฆ‰์‹œ `yaml` import ๋ฅผ YAML ํด๋ฐฑ ๋ถ„๊ธฐ๋กœ ์ด๋™, UUID ํ•ด๊ฒฐ ๊ฒฝ๋กœ๊ฐ€ PyYAML ์—†์ด ์™„์ฃผํ•จ์„ ์‹คํ–‰ ๊ฐ€๋“œ๋กœ ๊ณ ์ •(`atomic_yaml.py` ์˜ ์‹œ์Šคํ…œ PyYAML ์š”๊ตฌ๋Š” ์„ค๊ณ„์ƒ ์œ ์ง€). +- ํšŒ๊ท€ ๊ฐ€๋“œ 3์ข… ์‹ ์„ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ ๋ถ€ํ™œ ์ฐจ๋‹จ, `import yaml` AST ๊ฒ€์‚ฌ(์ง€์—ฐ import ํฌํ•จ), ์‹คํ–‰ ๊ฒฝ๋กœ ๊ฒ€์ฆ. +- ํšŒ๊ท€ ๋ฐ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ: **266/266 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 73fd2b9..d5ff662 100644 --- a/tests/test_tier1_unit.py +++ b/tests/test_tier1_unit.py @@ -338,3 +338,67 @@ def test_mqtt_with_retry_failure(mam_sandbox): with pytest.raises(ValueError, match="failing"): failing_func() assert len(calls) == 3 + + +def test_b10_no_agent_identities_reader_in_production(): + """B-10: agent_identities has no writer; no production code may read it.""" + import pathlib + root = pathlib.Path(__file__).resolve().parent.parent + targets = [ + root / ".agents" / "skills" / "lib_py" / "workspace_uuid.py", + root / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh", + root / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh", + root / ".agents" / "skills" / "lib.sh", + ] + offenders = [] + for f in targets: + for i, line in enumerate(f.read_text().splitlines(), 1): + if "agent_identities" not in line: + continue + if line.lstrip().startswith("#"): # ๊ธˆ์ง€ ๊ทœ์•ฝ์„ ์„œ์ˆ ํ•˜๋Š” ์ฃผ์„์€ ํ—ˆ์šฉ + continue + offenders.append(f"{f.name}:{i}: {line.strip()}") + assert not offenders, "agent_identities read path resurrected:\n" + "\n".join(offenders) + + +def test_b10_workspace_uuid_has_no_yaml_import(): + """B-10: no `import yaml` anywhere in workspace_uuid.py โ€” top-level OR lazy.""" + import ast, pathlib + src = (pathlib.Path(__file__).resolve().parent.parent + / ".agents" / "skills" / "lib_py" / "workspace_uuid.py") + tree = ast.parse(src.read_text()) + offenders = [] + for node in ast.walk(tree): # ast.walk โ†’ ์ค‘์ฒฉ ๊นŠ์ด ๋ฌด๊ด€ + if isinstance(node, ast.Import): + for a in node.names: + if a.name.split(".")[0] == "yaml": + offenders.append(f"line {node.lineno}: import {a.name}") + elif isinstance(node, ast.ImportFrom): + if (node.module or "").split(".")[0] == "yaml": + offenders.append(f"line {node.lineno}: from {node.module} import ...") + assert not offenders, "PyYAML dependency reintroduced:\n" + "\n".join(offenders) + + +def test_b10_find_workspace_uuid_runs_without_pyyaml(tmp_path): + """B-10: the executed resolution path must not need PyYAML.""" + import subprocess, sys, os, json, pathlib + stub = tmp_path / "noyaml" + (stub / "yaml").mkdir(parents=True) + (stub / "yaml" / "__init__.py").write_text('raise ImportError("PyYAML absent (stub)")\n') + skills = str(pathlib.Path(__file__).resolve().parent.parent / ".agents" / "skills") + ws = tmp_path / "ws"; ws.mkdir() + env = os.environ.copy() + env["PYTHONPATH"] = f"{stub}:{skills}" + env["WS_ABS"] = str(ws) + env["AGENT"] = "claude" + env["MAM_STATE_JSON"] = json.dumps({"herdr_sessions": []}) + env["YAML_PATH"] = str(tmp_path / "agent-sessions.yaml") + env["HOME_DIR"] = str(tmp_path) + env["CLAUDE_PROJECT_DIR"] = str(tmp_path / "projects") + r = subprocess.run( + [sys.executable, "-c", + "from lib_py.workspace_uuid import find_workspace_uuid_main; find_workspace_uuid_main()"], + capture_output=True, text=True, env=env) + 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}" +