docs(reports): archive phase 1 finalized implementation plan (plan-923e9403)

This commit is contained in:
Godopu committed 2026-09-06 16:29:06 +09:00
1 parent dfa7212ea4
commit 8def235dc7
1 file changed
+463
@@ -0,0 +1,463 @@
# 📑 Issue #5 Phase 1 구현 계획서 (Rev.2) — `lib_py/delegation/` 추출 및 장수 MQTT 연결 프로토타입
- **Job ID**: `9aae4233` (Rev.1: `923e9403`) / **Planner**: `planner-reviewer-claude-01`
- **반영한 이의제기**: `creator-claude-01`의 Challenge (Job `c0b9c987`) — D2 shim 부트스트랩의 컨텍스트 충돌
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5) — Phase 1 (`implementation_plan.md` Rev.4 §2 Phase 1 행, `task.md` P1-01~P1-03)
- **기준 커밋**: `fc0bd96` (branch `refactor`, working tree clean) / Python `3.9.6` / paho-mqtt `2.1.0` / herdr `0.8.2` / 전체 테스트 수집 **510**
- **문서 성격**: Worker가 그대로 실행할 수 있는 단계별 구현 명세. 모든 인용 행 번호는 라이브 코드로 직접 확인함(§1). Rev.1 대비 변경은 §0-1에 모아 두었고, 본문에는 이미 반영되어 있다.
---
## 0. 요약 (TL;DR)
1. **추출 대상은 `lib.sh` L1833–1941의 위임 블록 5개 함수뿐이다.** L1943 이후(`wait_for_tui_ready`, `send_keys_safe` 등)는 TUI 조작 헬퍼이며 Phase 1 비범위다.
2. **`scripts/{mqtt_common,registry,publish_event,job_subscriber}.py`의 파일 경로는 와이어 계약이다.** 에이전트에게 주입되는 지시문(`create_session.sh:483`, 래퍼 `:197/:318`), SKILL.md, BOOTSTRAP*.md, `deploy/install.sh:103`, 테스트 3종이 리터럴 경로로 호출한다. 구현체는 `lib_py/delegation/`으로 옮기되 `scripts/*.py`는 **동일 모듈 객체를 가리키는 호환 shim**으로 남긴다.
3. **shim은 `sys.path`를 건드리지 않고 `lib_py.delegation` 패키지만 자기 옆 트리에 "프로세스당 1회" 바인딩한다(Rev.2, D2).** 이의제기가 실증한 대로 `sys.path.insert(0, ...)` 방식은 pytest에서 위임과 무관한 `lib_py.*`까지 임시 사본으로 가로챈다. 이의제기가 제안한 "잎(leaf) 모듈 파일 직접 적재"도 재현 결과 비-잎 모듈(`publish_event`)에서 두 트리가 섞이므로 채택하지 않고, **패키지 단위 bind-once**로 확정했다(§1-8 근거).
4. **현재 셸 헬퍼 3종을 직접 검증하는 테스트는 0개다.** 이동 전에 특성화(golden) 테스트를 먼저 추가한다(P1-00).
5. **장수 연결 프로토타입은 라이브러리 전용이다.** CLI는 프로세스 단위로 종료되므로 재사용 이득이 없다. 벤치마크는 결정적 가짜 전송 + 실브로커(정보성) 2모드.
6. **자동완료 정책은 3개 표면에서 한 줄도 바뀌지 않는다.** 테스트로 고정한다.
### 0-1. Rev.1 → Rev.2 변경 내역 (이의제기 반영)
| 이의제기 항목 | Planner 독립 검증 결과 | 계획 변경 |
|---|---|---|
| §1 D2 `sys.path.insert(0)`이 pytest에서 `lib_py` 전체를 샌드박스로 고정 | **재현 확인** (§1-8 표, 변형 `d2_insert0` 시나리오 A: `lib_py.agents`가 sandbox에서 로드) | D2 전면 교체 |
| §2-1 정체성 단정(`is`)이 "같은 트리"를 증명하지 못함 | **동의**. 시나리오 A에서 두 참조가 모두 sandbox인데 단정은 `True` | D2-2 출처(provenance) 단정 PV-1~PV-4 추가, G4 재정의 |
| §2-2 R3 전제("실제 트리에서만 로드")가 D2와 모순 | **동의** | R3을 "수용"에서 "해소"로 재작성 |
| §3 `append`로 바꾸면 freeze가 깨짐 | **동의**. 본 세션 `PYTHONPATH`=라이브 트리, 실행 명령=freeze 경로, `run_loop.sh`에 `PYTHONPATH` 처리 0건 모두 재확인 | D2-1 컨텍스트 우선순위 표 신설 |
| §5-1 "옆 파일 직접 적재(`spec_from_file_location`) + 다르면 교체" | **부분 채택**. 잎 모듈 단독 임포트에서는 동작하나, `publish_event`만 임포트되는 실제 에이전트 호출 형태(C2_pe)에서 `publish_event`=freeze, `mqtt_common`=live로 **혼재**하고 정체성 단정이 `False`가 됨. 진짜 standalone(C1)에서는 부모 패키지 부재로 `ModuleNotFoundError`. 두 샌드박스가 섞이는 시나리오(D)에서도 혼재 | 적재 단위를 **모듈 → 패키지**로 올리고, "가장 가까운 사본으로 교체"가 아니라 **"프로세스당 최초 바인딩 유지(bind-once)"** 로 규칙 변경. standalone은 `lib_py` 부재 시에만 `sys.path.append` 폴백 |
| §5-2 출처 단정 병기 | 채택 | PV-1~PV-4 |
| §5-3 컨텍스트 4종 우선순위 표 + 4종 전부 테스트 | 채택 (6 컨텍스트로 확장) | D2-1, P1-01 테스트 |
| §5-4 세션 위생 테스트 | **부분 채택**. "tmp 경로가 `sys.path`에 없음"은 현행 `test_tier1_unit.py:23`이 샌드박스 scripts 디렉터리를 영구 삽입하므로 이미 실패함. 조건을 "`lib_py` 디렉터리를 포함하는 tmp 경로가 없음"으로 정밀화 | PV-3 |
| §6 D6·설치 경로 안전 확인 | **동의**. `mqtt_common.py`의 `os.environ` 참조는 전부 함수 내부(`:139,:196,:203,:225-233`), 모듈 상수(`:117-128`) env 무관, 나머지 3개 스크립트 `os.environ` 0건, `install.sh:127/131/135` 트리 통째 복사, `install_mam.sh:126` `.agents/` rsync 재확인 | 변경 없음 (P1-04의 `core_files` 추가는 무결성 목록 갱신 목적임을 명시) |
---
## 1. Planner 사전 실측 (라이브 검증 결과)
### 1-1. 추출 범위 경계 (`lib.sh`, 총 2312행)
| 행 | 함수 | 분류 | 비고 |
|---|---|---|---|
| 1833 | 섹션 주석 `multi-agent-mux-delegate-job integration helpers` | 경계 시작 | |
| 1841 | `_delegate_py_bin` | **셸 유지** | 파이썬 인터프리터 자체를 찾는 부트스트랩. L1598(`env_python`), L1660(`atomic_dump_yaml`), `reconcile.sh:67`, `create_session.sh:191/193/215/482`, `stop_session.sh:199`, `resume_session.sh:116/126`에서도 사용. `tests/test_c1_tui_readiness.py:168,513`이 mock으로 덮어씀 |
| 1865 | `_delegate_script` | **셸 유지** | 스크립트 경로 해석. shim이 남으므로 그대로 동작 |
| 1876 | `delegate_submit_job` | 추출 | `registry.py register` 호출, stdout에 JID만 출력 |
| 1897 | `delegate_publish_event` | 추출 | 비치명 계약: 빈 job_id/스크립트 부재/브로커 실패 모두 `return 0` |
| 1912 | `start_watchdog` | 추출 → `ensure_subscriber_daemon` | `pgrep -f "bash <monitor_script> --subscribe"` 선검사 후 `nohup bash reconcile.sh --subscribe --idle-timeout 0` 스폰, PID를 stdout 출력. 스크립트 부재 시 stderr `ERROR: monitor script not found: ...` + `return 1` |
| 1943~ | `wait_for_tui_ready`, `inject_instructions`, `_pane_*`, `send_keys_safe`, `handle_startup_dialogs` | **비범위** | TUI 조작 |
`lib_py`는 `lib.sh:25`(`SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` 직후 `export PYTHONPATH="$SKILL_DIR:..."`) 및 `:164`로 임포트 가능해진다. 즉 **어느 트리의 `lib.sh`를 source 했느냐가 그 셸 프로세스의 `lib_py` 트리를 결정**한다. `env_python`(L1583)은 `PYTHONPATH=$SKILL_DIR`를 명시 전달하는 선례.
### 1-2. 셸 헬퍼 호출자 (라이브 트리)
| 호출자 | 헬퍼 | 계약상 의존 |
|---|---|---|
| `create_session.sh:345` | `delegate_submit_job` | `$( )`로 stdout 캡처 → JID |
| `create_session.sh:497,501` | `delegate_publish_event error/started` | 비치명 |
| `create_session.sh:504-505` | `start_watchdog "$JOB" "$WORKSPACE"` | `$( )`로 PID 캡처 후 `echo`만 함(파싱 없음) |
| `stop_session.sh:193,242,322` | `delegate_publish_event progress/cancelled` | `implementation_plan.md` §3-2가 보존 요구 |
| `update_yaml_resumed.sh:257` | `delegate_publish_event progress "resumed"` | 비치명 |
| `run_loop.sh:144-155` | (헬퍼 미사용) 래퍼 `multi-agent-mux-delegate-job`를 직접 실행 | Phase 1 무관 |
### 1-3. 파이썬 표면과 리터럴 경로 계약
- 스크립트 4종: `mqtt_common.py`(670행), `registry.py`(426행), `publish_event.py`(239행), `job_subscriber.py`(339행). 서로를 `import mqtt_common` / `import registry`처럼 **최상위 이름**으로 임포트한다. **다른 `lib_py` 모듈은 하나도 임포트하지 않는다**(`grep -n lib_py scripts/*.py` → 0건; D2의 전제).
- 리터럴 경로 호출자: 래퍼 12개소(`:136,166,197,277,287,318,355,394,400,544,550,571,575,583,586`), `create_session.sh:483`, `SKILL.md:78`, `BOOTSTRAP.md`/`BOOTSTRAP.ko.md:158-180`, `README.md:211`/`README.ko.md:194`, `MESSAGING.md:399`, `deploy/install.sh:103`, `reconcile.sh:98-113`(임베디드 구독자가 scripts 디렉터리를 `sys.path`에 **append** 후 `import mqtt_common, registry`).
- 테스트 임포트 방식: `tests/test_tier1_unit.py:23,571-820`과 `tests/test_tier2_component.py:25`는 샌드박스 scripts 디렉터리를 `sys.path`에 넣고 `import registry, publish_event, job_subscriber, mqtt_common`; `tests/test_tier4_e2e.py:414`는 `publish_event.py`를 서브프로세스로 실행. `tests/conftest.py:10-12`는 **실제** skills 디렉터리를 `sys.path[0]`에 넣고, `mam_sandbox`는 `.agents/skills` 전체를 `tmp_path/skills`와 `tmp_path/.agents/skills`로 복사한다(이동 후 사본에도 `lib_py/delegation/`이 포함됨).
- 본 job과 Rev.1 job의 위임 메시지 자체가 `.../mam-loop-freeze.PfAQNj/.agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py` 경로를 사용한다. 이때 에이전트 세션의 `PYTHONPATH`는 **라이브 트리**다(실측: `/Users/.../multi-agent-mux/.agents/skills:...`). `run_loop.sh`에는 `PYTHONPATH` 처리가 없고(`grep` 0건) `:14-17`에서 자기 위치 기준 `REPO_ROOT`의 `lib.sh`를 source 하므로 **루프 프로세스 자체는 freeze `lib.sh`가 export한 freeze `PYTHONPATH`** 를 갖는다.
### 1-4. 기존 테스트 커버리지 실측
| 대상 | 커버 테스트 | 비고 |
|---|---|---|
| `publish_event` 실패 시 status/감사로그/seq 소비 | `test_tier1_unit.py` G-1~G-4 (:568-698) | 127.0.0.1:65432 비라우팅 포트, 네트워크 미접촉 |
| `job_subscriber` 디스크 폴백/exit 0·1·3 | G-5~G-9 (:700-836) | |
| 래퍼 rc=3 매핑 리터럴 | G-10 (:838) | **래퍼 텍스트를 건드리면 깨짐** |
| HMAC/retry/topic/reason_code | `test_mqtt_*` (:339-420) | |
| seq 단조 증가, 모니터 중복 잠금 | `test_tier2_component.py:1082, :1007` | |
| `delegate_submit_job` / `delegate_publish_event` / `start_watchdog` | **없음 (0건)** | |
빠른 회귀 부분집합(G-1~G-10 + mqtt + seq + monitor lock): **18 passed in 1.74s**. 이 부분집합의 파일들은 `lib_py`를 모듈 레벨에서 임포트하지 않으므로(`test_tier1_unit.py`의 `lib_py` 사용은 `:848,:873,:902,:947,:978,:1037,:1072` 전부 함수 내부) 이의제기 §4의 지적대로 "샌드박스 shim이 먼저 임포트되는" 시나리오 A에 해당한다 — Rev.2 D2는 이 경우에도 `lib_py.agents` 등을 가로채지 않는다(§1-8).
### 1-5. 자동완료 정책이 구현된 3개 표면 (Phase 1 불변)
1. `publish_event.py:51-56` `EVENT_TO_STATUS` 및 `:227`의 `update_job_status` 동기화.
2. 래퍼 `:335-348` 구독자 exit code → `job_status` 매핑 및 `:394/:400`의 `registry.py status --set completed|error`.
3. `reconcile.sh:125-168` `_MUTATION` + `handle_terminal`.
`run_loop.sh:238-276` `wait_for_job`은 `.mam/jobs/<id>.json`의 `status`를 3초 간격 폴링할 뿐이므로 영향 없음.
### 1-6. 장수 연결의 실측 동기
Rev.1 job의 `started` 이벤트: payload `timestamp` 05:58:49Z → `attempt 1/3 failed: no CONNACK from broker` (05:59:00Z) → 모니터 수신 05:59:01.487Z. 1회 발행 약 12.5초, 원인은 `CONNECT_ACK_TIMEOUT=10`(`publish_event.py:58`)의 1회 실패 + 재연결. Rev.1 `completed` 발행과 본 job의 `started` 발행에서도 동일한 1회 CONNACK 실패가 재현됐다.
### 1-7. 환경 제약
- 로컬 브로커 없음(`mosquitto`, `nats-server`, `docker` 미설치). 벤치마크 실브로커 모드는 정보성.
- `mqtt_common.py:112`에서 `_load_dotenv()`가 **임포트 시점**에 실행되어 `.mam.env`(존재, 0600)를 주입. 루트 탐색은 `__file__` 기준 상향(`.agents` 또는 `.git`).
- B-13 freeze(`run_loop.sh:112-126`)는 `.agents/skills` 전체를 `cp -R` 하므로 `lib_py/delegation/`은 자동 포함(현재 freeze에 `lib_py/` 존재 확인).
- `baseline.md` §5-2 `VALID_STATUSES` 인용은 커밋 `19d798b`에서 `registry.py:38`로 정정 완료(`baseline.md:93`).
### 1-8. 이의제기 재현 및 대안 비교 (Rev.2 신규)
세 shim 변형을 `real`/`sandbox`/`sandbox2`/`freeze` 네 트리(각 `lib_py/{__init__,agents/__init__,delegation/{__init__,mqtt_common,publish_event}}.py` + `scripts/` shim)에 배치하고, `env -u PYTHONPATH`의 새 인터프리터에서 시나리오별로 실행했다. `publish_event`는 `from lib_py.delegation import mqtt_common`을 포함해 **비-잎 모듈**을 모델링한다. 열: 어느 트리에서 로드됐는가.
| 시나리오 (첫 임포트 → 이후) | `d2_insert0` (Rev.1) | `worker_leaf_file` (이의제기 §5-1) | `pkg_bind_once` (**Rev.2 채택**) |
|---|---|---|---|
| A pytest: real on path → sandbox shim(mc, pe) | mc=sb, pe=sb, **agents=sb** ✗ | mc=sb, pe=sb, agents=real | mc=sb, pe=sb, agents=real ✓ |
| A_pe pytest: sandbox shim(**pe만**) | agents=sb ✗ | **pe=sb, pe.dep=real, identity=False** ✗ | pe=sb, pe.dep=sb, identity=True ✓ |
| B pytest: real `lib_py.agents` 먼저 → sandbox shim | 전부 real | mc=sb, pe=sb, agents=real | mc=sb, pe=sb, agents=real ✓ |
| B2 pytest: real `lib_py.delegation` 먼저 → sandbox shim | (미측정) | **alias=sb, pkg=real, identity=False** ✗ | 전부 real, identity=True ✓ |
| C1 standalone(`PYTHONPATH` 없음): freeze shim | 전부 freeze | **`ModuleNotFoundError: lib_py`** ✗ (비-잎 모듈의 내부 임포트가 부모 패키지를 요구) | 전부 freeze ✓ (폴백 append) |
| C2 freeze shim + `PYTHONPATH`=live (mc, pe) | 전부 freeze | 전부 freeze, agents=real | 전부 freeze, agents=real ✓ |
| C2_pe freeze shim(**pe만**) + `PYTHONPATH`=live — **실제 에이전트 호출 형태** | 전부 freeze | **pe=freeze, pe.dep=real, identity=False** ✗ (B-13 부분 상실) | pe=freeze, pe.dep=freeze ✓ |
| D 두 샌드박스: sb shim(mc) → sb2 shim(pe) | agents=sb ✗ | **pe=sb2, pe.dep=sb** 혼재 ✗ | 전부 sb (최초 바인딩 유지) ✓ |
| E sandbox shim(mc) → 이후 `from lib_py.delegation import publish_event` 직접 임포트 | (해당 없음) | 직접 임포트=real, shim=sb 혼재 | 둘 다 sb ✓ |
결론: 이의제기의 **결함 지적은 정확**하고, 제안한 적재 방식은 "잎 모듈을 먼저 임포트하는" 경우에만 성립한다. 결정 기준은 (i) `lib_py.delegation.*`가 한 프로세스 안에서 항상 **단일 트리**일 것, (ii) `lib_py`의 다른 서브패키지는 shim이 **절대 건드리지 않을 것**, (iii) `sys.path` 무변경(standalone 폴백 제외) — 이를 모두 만족하는 것은 패키지 단위 bind-once뿐이다. 재현 하니스는 `tests/fixtures/issue_5/phase1/shim_contexts.py`로 그대로 옮겨 P1-01 테스트에 사용한다(부록 B).
---
## 2. 설계 결정 (Design Decisions)
### D1. 패키지 배치 — 기존 이름 보존, 신규 추가
```
.agents/skills/lib_py/delegation/
__init__.py # 공개 API 재수출: DelegationService, ensure_subscriber_daemon, PersistentPublisher
mqtt_common.py # git mv (scripts/mqtt_common.py) — 내용 변경은 D6·D8 항목만
registry.py # git mv (scripts/registry.py)
publish_event.py # git mv (scripts/publish_event.py) — D7의 함수 분리만
job_subscriber.py # git mv (scripts/job_subscriber.py)
service.py # DelegationService 파사드 (신규)
daemon.py # ensure_subscriber_daemon (신규, start_watchdog 이식)
publisher.py # OneShotPublisher / PersistentPublisher (신규)
cli.py # python -m lib_py.delegation.cli {submit|publish|ensure-daemon} (신규)
__main__.py # cli.main 위임 (신규)
bench.py # 벤치마크 하니스 (신규)
```
- `git mv`로 이동해 이력을 보존한다. 이동한 4개 모듈 내부의 `import mqtt_common` / `import registry` / `from mqtt_common import ...`는 전부 `from lib_py.delegation import mqtt_common, registry` 형태로 바꾼다.
- **패키지 내부 임포트 규칙(테스트로 grep 고정)**: (a) 최상위 이름(`import registry` 등) 금지 — `lib_py.agents.registry`와의 혼동 차단; (b) **`lib_py.delegation` 밖의 어떤 `lib_py` 모듈도 임포트 금지**(`paths`, `state`, `atomic_yaml`, `agents`, `layout`, `workspace_uuid`) — D2에서 부모 `lib_py`와 `lib_py.delegation`이 서로 다른 트리일 수 있으므로(C2), 형제 모듈을 임포트하면 트리가 섞인다. 현재 스크립트는 이미 이 규칙을 만족한다(§1-3).
- Python 3.9 호환 필수: `match`, PEP 604 런타임 유니온, `zip(strict=)` 금지. 게이트에서 `compileall`.
### D2. `scripts/*.py` shim — `lib_py.delegation` 패키지를 자기 옆 트리에 프로세스당 1회 바인딩 (Rev.2)
4개 shim은 아래 블록을 **바이트 동일하게** 포함한다(`{mod}`만 다름). 공유 헬퍼 파일로 빼지 않는 이유: `import _bootstrap` 같은 최상위 이름이 첫 샌드박스에 캐시되어 다른 트리의 shim이 남의 헬퍼를 쓰게 되기 때문이다. 동일성은 테스트로 고정한다.
```python
#!/usr/bin/env python3
"""Compatibility shim — implementation lives in lib_py.delegation.{mod}.
Binds the lib_py.delegation *package* to the tree next to this file, once per
process, without touching sys.path. See docs/contracts/issue-5/... (D2)."""
import importlib, importlib.util, os, sys
_SKILLS = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
_PKG = "lib_py.delegation"
if _PKG not in sys.modules: # bind once per process; never rebind
try:
import lib_py # parent resolved normally; shims never rebind it
except ModuleNotFoundError:
sys.path.append(_SKILLS) # standalone only: no lib_py exists anywhere to shadow
import lib_py
_d = os.path.join(_SKILLS, "lib_py", "delegation")
_spec = importlib.util.spec_from_file_location(
_PKG, os.path.join(_d, "__init__.py"), submodule_search_locations=[_d])
_pkg = importlib.util.module_from_spec(_spec)
sys.modules[_PKG] = _pkg # register BEFORE exec so __init__'s own imports resolve here
_spec.loader.exec_module(_pkg)
lib_py.delegation = _pkg
_impl = importlib.import_module(_PKG + ".{mod}") # resolved through the bound package's __path__
if __name__ != "__main__":
sys.modules[__name__] = _impl # `import {mod}` is lib_py.delegation.{mod}
else:
sys.exit(_impl.main())
```
규칙(문서화·테스트 대상):
1. **바인딩 단위는 패키지**다. `lib_py.delegation`이 `sys.modules`에 없을 때만 shim 옆 트리의 `lib_py/delegation/`을 패키지로 등록하고, 이후 모든 `lib_py.delegation.*` 임포트는 그 `__path__` 안에서 해석된다. 잎/비-잎 구분 없이 단일 트리가 보장된다.
2. **bind-once, never rebind.** 이미 바인딩돼 있으면(다른 트리라도) 그대로 쓴다. "가장 가까운 사본으로 교체"는 기존 참조(`sys.modules['mqtt_common']` 별칭 등)와 새 객체를 갈라놓아 정체성·monkeypatch를 깨뜨린다(§1-8 B2·D). 네 컨텍스트 중 한 프로세스가 트리를 바꿔야 하는 경우는 없다.
3. **부모 `lib_py`는 절대 재바인딩하지 않고, `sys.path`도 건드리지 않는다.** 유일한 예외는 `lib_py`가 어디에도 없는 standalone(C1)이며, 이때만 자기 트리를 `append` 한다(가릴 대상이 없으므로 안전).
4. 결과적으로 pytest에서는 위임 패키지만 "최초 shim의 샌드박스 사본(또는 먼저 직접 임포트된 실제 트리)"에 바인딩되고 `lib_py.agents`/`layout`/`state` 등은 항상 실제 트리다. 샌드박스 사본은 `copytree`된 동일 내용이므로 검증 대상 코드는 같다(PV-1이 이를 단정).
5. `mqtt_common.py`의 PEP 562 `__getattr__("LOGS_DIR")`은 실제 모듈에 남아 별칭을 통해 노출. 실행 비트 유지, `--help` 출력 동일.
6. `_load_dotenv`(D6 이후 `ensure_env_loaded`)의 루트 탐색은 실모듈 `__file__` 기준이므로 바인딩된 트리의 루트(저장소 루트 또는 freeze 루트)가 된다 — 이동 전과 동일.
### D2-1. 실행 컨텍스트별 "어느 트리가 이겨야 하는가" (Rev.2)
| # | 컨텍스트 | `lib_py.delegation` 해석 주체 | 이겨야 하는 트리 | Rev.2 결과 | 테스트 |
|---|---|---|---|---|---|
| K1 | 루프 프로세스(`run_loop.sh`가 freeze `lib.sh` source → `PYTHONPATH`=freeze) 및 그 자식(래퍼, `-m lib_py.delegation.cli`) | `PYTHONPATH` | freeze | freeze | K1: `PYTHONPATH=<copy>` + `-m lib_py.delegation.cli --help` |
| K2 | 에이전트 세션(`PYTHONPATH`=live)이 위임 메시지의 **freeze 경로 스크립트**를 실행 | shim 옆 트리 | freeze (B-13) | freeze, `lib_py` 부모는 live(무해: D1(b)) | C2_pe |
| K3 | 라이브 헬퍼(create/stop/resume/reconcile; live `lib.sh` source) | `PYTHONPATH`=live 또는 shim 옆 트리(같음) | live | live | K3: 실제 트리에서 shim 실행 |
| K4 | standalone(`PYTHONPATH` 없음; BOOTSTRAP.md의 수동 명령, 외부 셸) | shim 폴백 append | 자기 트리 | 자기 트리 | C1, C1_pe |
| K5 | pytest 인프로세스(conftest가 실제 트리 삽입, 샌드박스 shim 임포트) | 최초 shim 또는 최초 직접 임포트 | 위임: 최초 바인딩(사본=동일 내용) / **나머지 `lib_py.*`: 실제 트리** | A, A_pe, B, B2, D, E | PV-1~PV-3 |
| K6 | pytest 서브프로세스(e2e `:414`, freeze 시뮬레이션) — 새 인터프리터, `PYTHONPATH`=live 상속 가능 | shim 옆 트리 | 샌드박스 | 샌드박스 | K6: 서브프로세스 JSON 출력 단정 |
### D2-2. 가드 — 정체성만으로는 불충분, 출처(provenance)를 병기 (Rev.2)
- **PV-1 (인프로세스, 각 shim 임포트 직후)**: `mqtt_common is lib_py.delegation.mqtt_common` **그리고** `Path(lib_py.delegation.__path__[0]).resolve()`가 `{실제 skills 디렉터리, pytest basetemp 하위}` 중 하나이며, 4개 별칭(`mqtt_common/registry/publish_event/job_subscriber`)의 `__file__`이 **모두 같은** `__path__[0]` 아래.
- **PV-2 (세션 종료 시, conftest의 session-scope autouse finalizer)**: `lib_py.__file__`, 그리고 `sys.modules`에 있는 모든 `lib_py.<x>` 중 `x != "delegation"`의 `__file__`이 실제 skills 디렉터리 아래. 위반 시 어느 모듈이 어느 tmp 경로에서 왔는지 출력.
- **PV-3 (세션 종료 시 위생)**: `sys.path` 항목 중 basetemp 하위인 것 가운데 `<entry>/lib_py`가 디렉터리인 항목 0개. (scripts 디렉터리 삽입은 `lib_py`를 담지 않으므로 허용 — 현행 `test_tier1_unit.py:23`과 호환.)
- **PV-4 (서브프로세스 매트릭스)**: §1-8의 시나리오 A, A_pe, B, B2, C1, C1_pe, C2, C2_pe, D, E를 실제 이동된 트리의 사본으로 재실행하고 JSON으로 트리명을 받아 단정. K1·K3·K6도 같은 하니스에 포함.
- **PV-5**: 4개 shim의 부트스트랩 블록이 `{mod}` 치환을 제외하고 바이트 동일.
### D3. 셸에 남는 것과 파이썬으로 가는 것
- `_delegate_py_bin`, `_delegate_script`: **셸 유지**. 시그니처·출력 불변.
- `delegate_submit_job`, `delegate_publish_event`, `ensure_subscriber_daemon`(신규 이름) + `start_watchdog`(별칭): 본체를 `python -m lib_py.delegation.cli ...` 호출로 교체. `PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}"`를 명시 전달(`env_python` L1583과 동일). `-m` 경로는 shim을 거치지 않으므로 D2-1의 K1/K3처럼 `SKILL_DIR` 트리가 결정한다.
- 롤백 스위치 `MAM_DELEGATION_PY`(기본 `1`): `0`이면 기존 인라인 본체(`_*_legacy`)로 분기. 특성화 테스트를 두 경로에 대해 parametrize. legacy 본체 제거는 Phase 4.
```bash
start_watchdog() { ensure_subscriber_daemon "$@"; } # 기존 함수 별칭 보존
ensure_subscriber_daemon() {
if [ "${MAM_DELEGATION_PY:-1}" = "0" ]; then _start_watchdog_legacy "$@"; return $?; fi
PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$(_delegate_py_bin)" -m lib_py.delegation.cli \
ensure-daemon --job "$1" --workdir "${2:-$PWD}"
}
delegate_publish_event() {
local job_id="$1" event="$2" detail="${3:-}"
[ -n "$job_id" ] || return 0 # 비치명 계약 1
if [ "${MAM_DELEGATION_PY:-1}" = "0" ]; then _delegate_publish_event_legacy "$@"; return $?; fi
PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$(_delegate_py_bin)" -m lib_py.delegation.cli \
publish --job "$job_id" --event "$event" --detail "$detail" || true # 비치명 계약 2·3
}
```
### D4. `cli.py`는 라우터, 로직은 기존 `main()` 재사용
- `publish` → `publish_event.main(argv)` 그대로 호출(exit 0/1/2, stdout 무출력, 옵션 전부 통과).
- `submit` → `registry.main(["--registry-dir", ..., "register", ...])`. stdout에 JID 한 줄, 로그는 stderr.
- `ensure-daemon` → `daemon.ensure_subscriber_daemon(job_id, workdir)`; stdout PID, exit 0 / 부재 시 stderr 동일 문구 + exit 1.
- 래퍼 `multi-agent-mux-delegate-job`(bash)은 **Phase 1에서 수정하지 않는다**(G-10 보호).
### D5. `daemon.ensure_subscriber_daemon(job_id, workdir) -> int` 의미 보존
| 항목 | 레거시(`lib.sh:1912-1941`) | 파이썬 이식 |
|---|---|---|
| 모니터 스크립트 경로 | `<workdir>/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh` | 동일 |
| 부재 시 | stderr `ERROR: monitor script not found: <path>`, rc 1 | 동일 문구·rc |
| 중복 검사 | `pgrep -f "bash <path> --subscribe"` | `subprocess.run(["pgrep","-f",...])` 동일 패턴 |
| 기존 PID 출력 | `echo "$pid"` (다중 매치면 여러 줄) | 동일하게 전량 출력(정리는 Phase 4 후보) |
| 스폰 | `nohup bash <path> --subscribe --idle-timeout 0 </dev/null >> <workdir>/.mam/multi-agent-mux-monitor.log 2>&1 &`, cwd=workdir | `Popen([...], cwd=workdir, stdin=DEVNULL, stdout=log(append), stderr=STDOUT, start_new_session=True, preexec_fn=SIGHUP 무시)` |
| 반환 | 스폰된 PID | 동일 |
| 데몬 자체 중복 방지 | `reconcile.sh:86-95` `.mam/monitor.lock` flock | 변경 없음 |
### D6. 임포트 시점 부작용 제거 — `_load_dotenv()`를 진입점 시점으로
`mqtt_common.py:112`의 모듈 레벨 `_load_dotenv()` 호출을 `ensure_env_loaded()`(멱등, 1회)로 바꾸고 **모든 프로세스 진입점** 첫 줄에서 호출: `publish_event.main`, `job_subscriber.main`, `registry.main`, `cli.main`, `bench.main`, `reconcile.sh` 임베디드 구독자(`import mqtt_common` 직후 1줄). 이의제기 §6이 독립 검증한 대로 `mqtt_common.py`의 env 참조는 전부 함수 내부이고 모듈 상수는 env 무관하므로 값이 달라지는 상수는 없다. `conftest.mam_sandbox`에 `monkeypatch.setenv("MAM_ENV_FILE", str(tmp_path / ".mam.env.absent"))` 추가(`_load_dotenv:48-52`는 명시 파일 부재 시 무로딩).
### D7. 발행 경로 분리 — 전송(transport)만 교체 가능하게
`publish_event.main()`(`:133-235`)을 동작 불변으로 세 조각으로 나눈다.
```python
def prepare_event(job_id, event, detail, data, registry_dir) -> PreparedEvent # load_job, broker cfg, topic, next_seq(소비), build_payload, retain 결정
def record_outcome(prepared, registry_dir, publish_ok, publish_error) -> None # append_event(audit) + registry.append_event + EVENT_TO_STATUS 동기화
def main(argv=None) -> int: # parse → ensure_env_loaded → prepare_event → with_retry(_publish_once) → record_outcome → 0/2
```
- G-1/G-2/G-4의 의미가 `record_outcome` 한 곳에 남는다.
- `publisher.OneShotPublisher.publish(prepared)` = 기존 `_publish_once`.
- `publisher.PersistentPublisher`: 컨텍스트 매니저. 1회 연결 + `loop_start()` + `reconnect_delay_set(1,16)`; `publish(prepared)`는 QoS1 발행 + `wait_for_publish(PUBLISH_ACK_TIMEOUT)`; 미확인 시 예외를 던져 호출자의 `with_retry`가 재시도. 브로커 설정이 다른 job 혼입 시 예외.
- `service.DelegationService(registry_dir, persistent=False)`: `submit_job()`, `publish_event()`, `ensure_subscriber_daemon()`. CLI/셸은 항상 `persistent=False`. Phase 2a 엔진이 `persistent=True`의 첫 소비자.
- 패리티 테스트: 동일 입력·가짜 전송에서 OneShot과 Persistent의 레지스트리(`last_seq`, `status`)·감사 레코드(`published`, `seq`, `retain`, `payload.data.hmac_sig`)가 필드 단위로 동일.
### D8. 벤치마크 — 결정적 지표와 정보성 지표 분리
`python -m lib_py.delegation.bench --n 50 --transport {fake|broker} [--connect-latency-ms 50] --out <json>`
| 지표 | fake 전송(CI에서 단정) | broker 전송(수동, 정보성) |
|---|---|---|
| 연결 수립 횟수 / N회 발행 | OneShot = N, Persistent = 1 (+재연결 수) — 테스트로 고정 | 동일 카운트 실측 |
| 발행당 지연 p50/p95/max | 합성 지연 기준 산술 확인 | 실측 (CONNACK 타임아웃 건수 별도) |
| 프로세스 포크 수(셸 경로) | `delegate_publish_event` 1회당 레거시 3 fork → 신규 2 fork | 분석값 |
결과는 `docs/contracts/issue-5/phase1-benchmark.md`에 환경과 함께 기록. 실브로커 수치는 게이트 조건이 아니다.
### D9. 비범위 및 호환성 계약(재확인)
- 저장소 통합 금지: `lib_py/delegation`은 `lib_py.state`/`lib_py.atomic_yaml`을 임포트하지 않는다 — D1(b)가 이를 포함하는 더 넓은 규칙.
- 스키마 불변: `SCHEMA_VERSION = 1`, 레코드 필드, 토픽, 감사 로그 레이아웃, `<id>.events.log` 형식 전부 불변 → §3 라이브 헬퍼 버전 게이트상 ABI 파괴 없음. K2에서 부모 `lib_py`(live)와 위임 패키지(freeze)가 다른 트리인 것은 D1(b) 덕분에 관측 가능한 차이를 만들지 않는다.
- `job_subscriber`는 이미 실행당 장수 연결이므로 전송 변경 없음.
- 자동완료 관련 코드 일체 추가 금지.
---
## 3. 작업 분해 (WBS) — 순서 고정, 커밋 5개
### P1-00 특성화 테스트 선행 (커밋 1: `test(issue-5): phase 1 delegation characterization suite`)
- **파일**: `tests/test_issue5_phase1_delegation.py`(신규), `tests/conftest.py`(`MAM_ENV_FILE` 1줄 + PV-2/PV-3 session finalizer), `tests/fixtures/issue_5/phase1/`(가짜 `reconcile.sh`, 가짜 `python` 스텁, `shim_contexts.py` 하니스 — 부록 B).
- **셸 헬퍼 golden 테스트** (샌드박스에서 `source lib.sh` 후 실행, `_delegate_py_bin`은 `test_c1_tui_readiness.py:513` 방식으로 스텁):
- C-01 `delegate_publish_event "" started` → rc 0, python 미실행.
- C-02 스크립트 부재 → rc 0.
- C-03 python 스텁 exit 2 → rc 0.
- C-04 정상 경로 argv에 `--job <id> --event <ev> --detail <detail>` 정확히 포함.
- C-05 `delegate_submit_job p a s` → stdout이 스텁의 JID 한 줄과 정확히 동일.
- C-06 `start_watchdog` 스크립트 부재 → stderr 정확 문구, rc 1.
- C-07 신규 스폰 → stdout 숫자 PID, 생존, cwd=workdir, 로그 파일 생성.
- C-08 두 번째 호출 → 동일 PID, 프로세스 수 불변.
- C-09 `start_watchdog` ≡ `ensure_subscriber_daemon` — P1-02 이후 활성.
- `MAM_DELEGATION_PY` ∈ {`0`,`1`} parametrize.
- **CLI 계약 스냅샷**: argparse 파서에서 옵션명·choices·default를 추출해 단정.
- **자동완료 불변 가드**: `EVENT_TO_STATUS` 동등; 래퍼 `:335-348` 리터럴; `reconcile.sh` `_MUTATION` 핵심 4줄 리터럴.
- **PV-2/PV-3 finalizer는 이 커밋에서 이미 활성** — 현행 트리에서 통과해야 한다(shim 도입 전 기준선).
- **완료 기준**: 신규 테스트 전부 통과(레거시 코드 그대로), 전체 스위트 510 + 신규 통과.
### P1-01 패키지 생성·이동·shim (커밋 2: `refactor(issue-5): move delegation scripts into lib_py.delegation with package-bound shims`)
- **파일**: `git mv` 4종, 내부 임포트 교정, `lib_py/delegation/__init__.py`, `scripts/*.py` 4종 shim(D2 블록), `mqtt_common.ensure_env_loaded()`(D6) + 4개 `main()` 및 `reconcile.sh` 임베디드 구독자 1줄.
- **추가 테스트**(Rev.2 확장): PV-1(4 별칭), PV-4 서브프로세스 매트릭스 10 시나리오 + K1/K3/K6, PV-5 shim 동일성, "라이브러리 임포트 후 `os.environ` 키 집합 불변", `ensure_env_loaded` 멱등, D1(a)(b) 임포트 규칙 grep, `reconcile.sh --subscribe --idle-timeout 1`이 `test_comp_monitor_concurrency_lock`과 동일 동작.
- **완료 기준**: 전체 스위트 통과, `git diff --check`, `compileall`, 3개 shim `--help` 출력이 이동 전 캡처본과 diff 없음, **PV-2/PV-3가 전체 스위트 실행 후에도 통과**(이것이 이의제기가 요구한 "어느 트리를 검증했는지" 증빙).
### P1-02 서비스·CLI·데몬 + 셸 헬퍼 교체 (커밋 3: `feat(issue-5): DelegationService, cli router, ensure_subscriber_daemon; lib.sh helpers delegate to python`)
- **파일**: `service.py`, `daemon.py`, `cli.py`, `__main__.py`; `lib.sh` L1876-1941 교체(D3).
- **추가 테스트**: daemon 단위(PID 생존·중복·로그 경로·부모 종료 후 생존), `cli publish` ≡ `publish_event.main`(G-1~G-4 cli 경로 parametrize), `cli submit` stdout, C-01~C-09 신규 본체 재통과.
- **완료 기준**: 두 경로 특성화 통과, `bash -n` 6개 파일, 전체 스위트 통과.
### P1-03 장수 연결 프로토타입 (커밋 4: `feat(issue-5): persistent MQTT publisher prototype and benchmark harness`)
- **파일**: `publish_event.py` 함수 분리(D7), `publisher.py`, `service.py` `persistent` 옵션, `bench.py`.
- **추가 테스트**: 가짜 paho 클라이언트로 (a) OneShot N회 = N connect, Persistent = 1, (b) 패리티, (c) PUBACK 미확인 → 재시도 → `record_outcome(published=False)`, (d) 브로커 설정 상이 혼입 예외, (e) `bench --transport fake` 스키마·카운트. 네트워크 접촉 0.
- **완료 기준**: 위 테스트 통과, G-1~G-4 동일 통과, 전체 스위트 통과.
### P1-04 벤치마크 실측 및 문서 (커밋 5: `docs(issue-5): phase 1 benchmark results, contract mapping, task checklist`)
- 실행: fake 모드 + broker 모드 1회(임시 레지스트리). `docs/contracts/issue-5/phase1-benchmark.md`에 기록.
- 문서 갱신: `SKILL.md`(version `4.2.0`→`4.3.0`, "구현은 `lib_py.delegation`, `scripts/*`는 패키지 바인딩 shim" 1단락 + D2-1 표 요약), 스킬 `README.md`, `registry.md`/`job-protocol.md` 각주, `deploy/install.sh` `core_files`에 `.agents/skills/lib_py/delegation/__init__.py` 추가(무결성 목록 갱신 목적; 배포 자체는 트리 통째 복사라 누락 위험 없음), `baseline.md` §6 매핑 행, `task.md` P1-01~03 체크 + 증거, `implementation_plan.md` Phase 1 행 상태.
- 라이브 스모크(수동, 보고서 기록): 임시 레지스트리에서 `registry.py register` → shim `publish_event.py --event started` → `-m lib_py.delegation.cli publish --event completed` → `<id>.events.log` 2줄, `events.ndjson` `published` 2건, `status.json`=completed. 이어 `MAM_DELEGATION_PY=0`으로 반복. **추가(Rev.2)**: 에이전트 세션과 같은 조건(`PYTHONPATH`=live)에서 **freeze 사본 경로**의 shim을 실행하고 `python -c`로 `lib_py.delegation.__path__`가 freeze 사본임을 출력해 첨부(K2 실증).
---
## 4. 계약 보존 매트릭스
| 계약 | 구현 위치(이동 후) | 보장 테스트 |
|---|---|---|
| 스크립트 리터럴 경로·실행 비트·`--help` | `scripts/*.py` shim | P1-01 `--help` diff, K6, e2e `:414` |
| `import mqtt_common` 등 최상위 이름 → 단일 트리의 동일 객체 | shim 패키지 바인딩 + 별칭 | **PV-1(정체성+출처)**, PV-4 |
| 위임 외 `lib_py.*`는 항상 실제 트리(pytest) / 부모 무재바인딩 | shim 규칙 3 | **PV-2, PV-3** |
| `publish_event` exit 0/1/2, stdout 무출력 | `publish_event.main` 불변 | G-1~G-4 + cli parametrize |
| seq 단조·실패 시 소비 | `prepare_event`→`next_seq` | G-4, `test_comp_monitor_monotonic_seq_updates` |
| HMAC `data.hmac_sig` | `build_payload` 단일 경로 | `test_mqtt_verify_hmac_*`, 패리티 |
| 터미널 이벤트 retain | `prepare_event` | 패리티(`retain`) |
| `job_subscriber` exit 0/1/2/3, 디스크 폴백 3s | 이동만 | G-5~G-9 |
| 셸 헬퍼 비치명 | 셸 가드 + `\|\| true` | C-01~C-04 (두 경로) |
| `delegate_submit_job` stdout=JID 1줄 | `cli submit` + stderr 로깅 | C-05 |
| 데몬 PID/수명/중복/로그 경로 | `daemon.py` | C-06~C-09, daemon 단위 |
| `start_watchdog` 이름 | 별칭 | C-09 |
| 감사 로그 레이아웃·`DELEGATE_JOB_LOGS_DIR` | `mqtt_common` 불변 | G-2, G-3 |
| env 로딩 순서(OS env 우선), 라이브러리 임포트 무부작용 | `ensure_env_loaded`(진입점) | env 테스트 2종 |
| 자동완료 3표면 불변 | 변경 없음 | 불변 가드 3종 |
| 래퍼 텍스트 불변 | 변경 없음 | G-10 + 확장 |
| 위임 패키지의 형제 `lib_py` 모듈 무임포트 | D1(b) | grep 테스트 |
| freeze(B-13) — K1/K2 모두 freeze 트리 | `PYTHONPATH`(K1) / shim 바인딩(K2) | K1, C2, C2_pe, P1-04 실증 |
---
## 5. 게이트
| # | 조건 | 측정 방법 |
|---|---|---|
| G1 | 기존 510개 + 신규(예상 40±5) 전부 통과, skipped 0 | `.venv/bin/python -m pytest tests/ -q` (≈15분, 백그라운드) |
| G2 | 셸 문법·공백 클린 | `bash -n` 6개 파일, `git diff --check` |
| G3 | Python 3.9 컴파일 | `compileall -q .agents/skills/lib_py/delegation` |
| **G4** | **정체성+출처(PV-1), 위임 외 `lib_py` 실제 트리·`sys.path` 위생(PV-2/PV-3, 전체 스위트 종료 시점), 컨텍스트 매트릭스(PV-4 10+3), shim 동일성(PV-5), env 무오염** | P1-01 테스트 + conftest finalizer |
| G5 | 데몬/PID 계약 | C-06~C-09 두 경로 |
| G6 | 자동완료 3표면 불변 | 불변 가드 + `git diff --stat`(래퍼 무변경, `reconcile.sh`는 env 1줄만) |
| G7 | 벤치 결정적 지표 + 실측 문서 | fake 전송 테스트, `phase1-benchmark.md` |
| G8 | 롤백 드릴 | `MAM_DELEGATION_PY=0` + 라이브 스모크 |
| G9 | 라이브 스모크(shim·cli·**K2 freeze 사본**) 감사 로그 정합 | §3 P1-04 |
Worker 완료 보고서에는 G1 pytest 요약 원문, G4의 PV-2/PV-3 finalizer 통과 로그, G7 표, G9 발췌(K2 `__path__` 출력 포함)를 그대로 붙인다. file:line 인용은 `grep -n` 출력으로 뒷받침.
---
## 6. 롤백 절차
1. **즉시**: `export MAM_DELEGATION_PY=0` → 셸 헬퍼 legacy 분기. 패키지·shim은 그대로 두어도 무해.
2. **커밋 단위**: 5→4→3 순 `git revert`; 커밋 2까지 되돌리면 이동 전 상태. 커밋 1은 레거시에서도 통과.
3. **진행 중 루프**: freeze 스냅샷은 시작 시점 트리이므로 롤백은 다음 루프부터. 라이브 헬퍼는 즉시 legacy. 스키마 불변이므로 혼재 호환.
---
## 7. 리스크와 완화
| # | 리스크 | 완화 |
|---|---|---|
| R1 | 모듈 사본 분기(테스트 shim vs 프로덕션 패키지) | D2 패키지 바인딩 + PV-1 출처 단정 |
| R2 | 임포트 시 `.mam.env` 주입 오염 | D6 + conftest `MAM_ENV_FILE` |
| **R3 (Rev.2 재작성)** | shim 부트스트랩이 pytest에서 위임 외 `lib_py`를 tmp 사본으로 가로채거나(insert), freeze에서 live에 밀리거나(append), 비-잎 모듈에서 두 트리가 섞임(잎 파일 적재) | **해소**: 패키지 단위 bind-once, `sys.path` 무변경, 부모 무재바인딩. PV-2/PV-3/PV-4가 회귀를 검출 |
| R4 | Python 3.10+ 문법 | G3 |
| R5 | 공용 브로커 불안정 | 테스트는 비라우팅 포트/가짜 전송만 |
| R6 | `pgrep -f` 패턴 | 레거시 그대로, C-08 |
| R7 | `$( )` 캡처 stdout 오염 | stderr 로깅, C-05/C-07 |
| R8 | 래퍼 리터럴(G-10) 파손 | 래퍼 무수정 |
| **R9 (Rev.2 수정)** | `lib.sh` 미소싱/`PYTHONPATH` 부재 환경에서 shim·`-m` 실패 | shim: `lib_py` 부재 시에만 자기 트리 `append`(K4). `-m` 경로: 셸 헬퍼는 항상 `lib.sh` 내부이므로 `SKILL_DIR` 존재 + 명시 전달 |
| R10 | `lib_py.agents.registry` 혼동 | D1(a) grep |
| R11 | `start_new_session` vs `nohup` | SIGHUP 무시, 생존 테스트 |
| **R12 (신규)** | K2에서 부모 `lib_py`(live)와 위임 패키지(freeze)가 다른 트리 | D1(b): 위임 패키지는 형제 `lib_py` 모듈을 임포트하지 않음(grep 고정). 현재 0건 |
| **R13 (신규)** | 두 트리의 위임 모듈을 한 프로세스에서 동시에 써야 하는 경우 | 존재하지 않음(K1~K6 전수). 발생 시 bind-once 규칙상 최초 트리 유지 — 문서화 |
| **R14 (신규)** | pytest에서 위임 패키지가 샌드박스 사본에 바인딩되어 "실제 트리 미검증" 우려 | 사본은 `copytree` 동일 내용이며 PV-1이 사본 위치를 basetemp로 한정. K6 서브프로세스가 사본 경로 실행을 별도 검증 |
---
## 8. 명시적 비범위
- `lib.sh` L1943 이후 TUI 헬퍼의 파이썬화.
- 래퍼, `run_loop.sh`, `reconcile.sh`(env 1줄 제외) 수정.
- `job_subscriber`/모니터 전송 변경, 스키마 v2, 토픽 변경.
- 자동완료 로직 일체(Phase 2a/2b).
- 저장소 통합, Windows, `pgrep` 다중 PID 정리(Phase 4 후보).
- 기존 tier1/tier2 테스트의 임포트 방식 변경(회귀 오라클 보존).
---
## 9. Worker 인수인계 체크리스트
- [ ] P1-00 커밋 1: 특성화·스냅샷·불변 가드 + **PV-2/PV-3 finalizer**가 레거시 코드에서 통과
- [ ] P1-01 커밋 2: `git mv` 4종, D2 shim 4종(PV-5 동일), `ensure_env_loaded`, PV-1/PV-4 매트릭스, 전체 스위트 녹색 **+ finalizer 녹색**
- [ ] P1-02 커밋 3: `service/daemon/cli`, `lib.sh` 교체(legacy 보존, `MAM_DELEGATION_PY`), 두 경로 특성화 녹색, `bash -n`
- [ ] P1-03 커밋 4: `prepare_event/record_outcome`, `PersistentPublisher`, 패리티·카운트, 네트워크 0
- [ ] P1-04 커밋 5: 벤치 문서, SKILL/README/registry.md/job-protocol.md/install.sh/baseline §6/task.md/implementation_plan.md, K2 실증 첨부
- [ ] 완료 보고서: G1 원문, G4 finalizer 로그, G7 표, G9 발췌, `grep -n` 인용
- [ ] 커밋 접두: `test(issue-5)`, `refactor(issue-5)`, `feat(issue-5)`, `feat(issue-5)`, `docs(issue-5)`
---
## 부록 A. 계획 수립 시 실행한 검증 명령 (Rev.1 + Rev.2)
```bash
awk 'NR>=1800 && /^[a-zA-Z_][a-zA-Z0-9_]*\(\)/ {print NR": "$0}' .agents/skills/lib.sh
grep -rn 'delegate_publish_event\|delegate_submit_job\|start_watchdog\|_delegate_py_bin' --include='*.sh' --include='*.py' .agents tests
grep -l 'delegate_publish_event\|start_watchdog\|delegate_submit_job' tests/*.py # → (없음)
grep -n 'lib_py' .agents/skills/multi-agent-mux-delegate-job/scripts/*.py # → (없음) D1(b) 전제
echo "$PYTHONPATH" # 에이전트 세션 = 라이브 트리
grep -n PYTHONPATH .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh # 0건
sed -n '24,25p;163,164p' .agents/skills/lib.sh # SKILL_DIR 기준 PYTHONPATH export
ls /var/folders/.../mam-loop-freeze.PfAQNj/.agents/skills/lib_py # freeze에 lib_py 존재
grep -n 'os.environ' .agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py # 전부 함수 내부
.venv/bin/python -m pytest tests/ --collect-only -q | tail -1 # 510
```
## 부록 B. shim 컨텍스트 재현 하니스 (P1-01 PV-4의 원형)
세 변형 × 시나리오를 새 인터프리터(`env -u PYTHONPATH`)에서 실행하고 각 모듈의 `TREE` 마커를 출력한다. Worker는 이 하니스를 `tests/fixtures/issue_5/phase1/shim_contexts.py`로 옮기고, 가짜 트리 대신 **실제 이동된 트리의 사본**(`copytree`)을 `real/sandbox/sandbox2/freeze`로 사용하며, `TREE` 마커 대신 `__file__`/`__path__` 접두어로 트리를 판별하도록 바꾼다.
```python
# 트리 구성: <tree>/lib_py/{__init__.py, agents/__init__.py(TREE), delegation/{__init__.py, mqtt_common.py(TREE), publish_event.py(TREE + from lib_py.delegation import mqtt_common)}}
# <tree>/multi-agent-mux-delegate-job/scripts/{mqtt_common,publish_event}.py ← D2 shim
# 시나리오 (sys.path 조작은 conftest/tier1/실행 방식을 모델링):
# A : insert(0, real); insert(0, sandbox/scripts); import mqtt_common, publish_event
# A_pe : 같음, import publish_event 만
# B : insert(0, real); import lib_py.agents; insert(0, sandbox/scripts); import mqtt_common, publish_event
# B2 : insert(0, real); import lib_py.delegation.mqtt_common; insert(0, sandbox/scripts); import mqtt_common, publish_event
# C1 : insert(0, freeze/scripts); import mqtt_common, publish_event (PYTHONPATH 없음)
# C1_pe: 같음, publish_event 만
# C2 : insert(0, real); insert(0, freeze/scripts); import mqtt_common, publish_event
# C2_pe: 같음, publish_event 만 ← 에이전트 호출 형태
# D : insert(0, real); insert(0, sandbox/scripts); import mqtt_common; insert(0, sandbox2/scripts); import publish_event
# E : insert(0, real); insert(0, sandbox/scripts); import mqtt_common; from lib_py.delegation import publish_event
# 단정: 각 시나리오에서 {alias_mc, pkg_mc, pe, pe.dep} 가 단일 트리이고, pytest 계열(A,A_pe,B,B2,D,E)에서 agents == real,
# identity(alias is pkg) == True, sys.path 에 lib_py 를 담은 비-real 루트가 없음(C1 계열 제외).
```