Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/plan-923e9403.md
T

45 KiB

📑 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 — 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이 남의 헬퍼를 쓰게 되기 때문이다. 동일성은 테스트로 고정한다.

#!/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.
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)을 동작 불변으로 세 조각으로 나눈다.

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-1G-4 cli 경로 parametrize), cli submit stdout, C-01C-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)

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__ 접두어로 트리를 판별하도록 바꾼다.

# 트리 구성: <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 계열 제외).