diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-744ac67a.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-744ac67a.md new file mode 100644 index 0000000..afb98eb --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-744ac67a.md @@ -0,0 +1,386 @@ +# 44062a63 — `BaseAgentAdapter` 아키텍처 설계 **Rev.2** + +**Job**: 44062a63 · **Role**: Planner · **Supersedes**: 744ac67a (Rev.1) +**응답 대상**: 챌린지 `c52bb834` (`agy`, `[CHALLENGE: RAISED]`) +**Base**: `245abe6` + +--- + +## 1. 판정 요약 + +**본 이의 1건과 보충 제언 2건 모두 채택한다.** 그리고 셋 중 둘은 agy 가 말한 것보다 **나쁘다**. + +| # | 항목 | 판정 | 실측 | +|---|---|---|---| +| C-1 | `candidate_uuids` 서명에 `cwd`/`epoch`/`claimed_uuids` 누락 | **채택 — 증상은 예측보다 위험** | agy 는 `[]` 를 예측했으나 실제는 **타 워크스페이스 대화가 유효 후보로 반환**된다 | +| M-1 | `PYTHONPATH` 부트스트랩 부족 | **우려 채택 · 제안 기각** | `run_loop.sh` 의 맨 `python3 -c` 5곳에서 `ModuleNotFoundError` 재현. 단 제안한 `__init__.py` 내 `sys.path.insert` 는 **순환이라 실행 자체가 불가능** | +| M-2 | `ready_tokens` 어댑터 이관 | **채택 — 효과가 예측보다 큼** | "2~3곳"이 아니라 `wait_for_tui_ready` 의 **25줄 case 블록 하나**가 데이터 조회 1줄로 바뀐다 | + +정정부터. Rev.1 §4.2 의 `candidate_uuids(ws_key, home, claude_dir, iso_root="")` 는 내가 +claude 의 디렉터리 구조만 보고 서명을 뽑은 결과다. agy·hermes·cline 은 **절대경로 `cwd`** 로 +스코프하는데 그 인자가 아예 없었다. agy 가 정확히 짚었다. + +**측정 결과: Rev.2 어댑터 4종 전부 `discover()` 정확. 변이 6건 전부 검출. 전체 회귀 162 passed, 0건.** + +--- + +## 2. C-1 — 채택. 다만 실패 양상이 예측과 다르다 + +### 2.1 agy 의 예측 vs 실제 + +agy 는 "`lc_data.get(ws_key)` 조회 실패 → 항상 `[]` 반환"이라고 봤다. +그런데 Rev.1 프로토타입의 agy 어댑터는 `last_conversations.json` 을 **아예 보지 않는다.** +conversations 디렉터리를 통째로 glob 한다. 실행해 봤다: + +``` +agy .candidate_uuids -> ['agy-mine', 'agy-foreign'] +hermes.candidate_uuids -> ['herm-mine', 'herm-foreign', 'herm-ancient'] + +기대: agy -> ['agy-mine'] (agy-foreign 은 /work/other 소속) + hermes -> ['herm-mine'] (herm-foreign 은 타 cwd, herm-ancient 는 세션 생성 이전) +``` + +`[]` 는 **서비스 거부**다. 지금 나오는 값은 **격리 위반**이다. 후자가 훨씬 나쁘다. +b4a1d094 이후 이 저장소가 계속 방어해 온 바로 그 부류의 결함이다. + +### 2.2 `verify_artifact` 도 막아 주지 않는다 + +`len(valid_candidates)==1` 게이트가 걸러 줄 거라 기대할 수도 있지만, 검증 단계를 실측했다: + +``` +agy agy-foreign -> True +hermes herm-foreign -> True +hermes herm-ancient -> True +``` + +전부 통과한다. 그러면 두 결말뿐이다 — 후보가 1개면 **남의 대화를 고정**하고, +2개면 b107cf34 에서 없앤 **영구 교착**으로 되돌아간다. 둘 다 받아들일 수 없다. + +여기서 agy 가 언급하지 않은 두 번째 결함이 나온다. **`verify_artifact(path, uuid, cwd)` 는 +`cwd` 를 이미 인자로 받고 있는데 agy·hermes 분기가 그걸 쓰지 않는다.** C-1 은 발견 단계만 +지적했지만 검증 단계도 같은 병을 앓고 있었다. + +### 2.3 hermes 는 파일 mtime 으로 epoch 을 걸 수 없다 + +agy 의 권고안은 `epoch` 을 인자로 넘기라고만 한다. 그런데 hermes 는 **모든 세션이 하나의 +`state.db` 를 공유**한다. 파일 mtime 은 후보 전체에 대해 같은 값이므로 mtime 기반 필터는 +"전부 통과" 아니면 "전부 탈락" 두 가지 답만 낼 수 있다. + +hermes 는 `sessions.started_at` 을 갖고 있으므로 그걸 써야 한다. 이건 **어댑터별 오버라이드 +지점**이고, 평평한 인자 목록만으로는 드러나지 않는다. + +> **미검증 항목**: hermes 는 이 머신에 설치돼 있지 않다(`command not found`). +> `sessions(id, cwd, started_at)` 스키마는 `lib.sh:1454` 의 실제 쿼리와 `tests/conftest.py` +> 의 mock 정의에서 역산한 것이다. 실 CLI 대조는 구현자 몫이다. + +--- + +## 3. C-1 설계 — 평평한 인자 대신 컨텍스트 객체 + +agy 의 권고안은 인자 7개짜리 서명이다. 방향은 맞지만 형태를 바꾼다. + +```python +@dataclass(frozen=True) +class DiscoveryContext: + cwd: str # /Users/x/proj -- agy, hermes, cline + ws_key: str = "" # -Users-x-proj -- claude + home: str = "" + claude_dir: str = "" + iso_root: str = "" + epoch: float = 0.0 # 0 이면 필터 비활성 + claimed: frozenset = frozenset() +``` + +**이유**: 이 서명은 **두 번의 리뷰에서 두 번 바뀌었다**(Rev.1 → `cwd` 추가 → `epoch`/`claimed` 추가). +위치 인자 목록은 바뀔 때마다 어댑터 4개 + 모든 호출부를 함께 고쳐야 한다. +세 번째 변경이 없으리라 가정할 근거가 없다. + +`cwd` 와 `ws_key` 를 **둘 다** 담는 것이 핵심이다. 둘은 교환 가능하지 않다 — +claude 는 `ws_key` 로 디렉터리를 찾고, agy(`last_conversations.json`)·hermes(`sessions.cwd`)· +cline(세션 json 의 `cwd`)은 절대경로로 찾는다. 하나만 넘기면 어느 쪽이든 반이 깨진다. + +### 3.1 필터는 어댑터가 아니라 기반 클래스에 둔다 + +```python +def discover(self, ctx) -> list: + out = [] + for uuid in self._raw_candidates(ctx): + if uuid in ctx.claimed: + continue + if ctx.epoch and not self._passes_epoch(uuid, ctx): + continue + out.append(uuid) + return out + +@abstractmethod +def _raw_candidates(self, ctx) -> list: ... # cwd 스코프만 책임진다 + +def _passes_epoch(self, uuid, ctx) -> bool: # 기본: 아티팩트 mtime + ... +``` + +agy 의 권고는 "`epoch` 필터링과 `claimed` 배제를 어댑터 내부에 캡슐화"였다. +**어댑터 4개가 각자 구현하면 잊어버릴 기회가 4번 생긴다.** 방금 그 방식으로 +agy·hermes 두 개가 `cwd` 를 잊은 것을 봤다. 그래서 필터는 기반 클래스가 갖고, +어댑터는 **스코프된 원시 후보만** 내놓는다. hermes 만 `_passes_epoch` 를 오버라이드한다(§2.3). + +### 3.2 agy 는 두 번째 방어선이 없다 — 그리고 그건 HEAD 도 마찬가지다 + +Rev.2 를 돌리면 agy 만 검증에서 foreign 을 못 막는다: + +``` +agy verify_artifact(foreign) -> True +claude verify_artifact(foreign) -> False +cline verify_artifact(foreign) -> False +hermes verify_artifact(foreign) -> False +``` + +내 설계 탓인지 확인하려고 **HEAD 의 셸 구현을 직접 호출**했다: + +``` +HEAD verify_session_uuid(agy, agy-mine) = True +HEAD verify_session_uuid(agy, agy-foreign) = True ← 동일 +``` + +agy 의 `.db` 에는 cwd 가 기록되지 않는다. 캐시가 유일한 스코프 수단이고, +HEAD 규칙은 "캐시가 인정하거나, 형제 세션이 점유하지 않았으면 통과"다. 어댑터도 그 규칙을 그대로 옮겼다. +**따라서 agy 에 대해서는 `_raw_candidates` 의 cwd 스코핑이 유일한 방어선이다.** +캐시에 이 cwd 항목이 없으면 `[]` 를 반환하도록 명시적으로 정했다 — +`[]` 는 고정을 지연시키지만, 전량 반환은 남의 대화를 고정한다. + +### 3.3 최종 인터페이스 + +```python +class BaseAgentAdapter(ABC): + name: str = "" + own_key: str = "" + supports_assigned_id = False + ready_tokens: tuple = () # M-2 + + def auth_ok(self, run) -> bool: ... + def spawn_spec(self, binary, session_uuid) -> SpawnSpec: ... + def resume_spec(self, binary, uuid, materialized) -> SpawnSpec: ... + + def artifact_path(self, uuid, ctx) -> str: ... + def artifact_exists(self, uuid, ctx) -> bool # 구체 구현 + def verify_artifact(self, uuid, ctx) -> bool: ... # cwd 를 반드시 쓸 것 + + def discover(self, ctx) -> list # 구체 구현 (템플릿) + def _raw_candidates(self, ctx) -> list: ... # 추상 + def _passes_epoch(self, uuid, ctx) -> bool # 오버라이드 가능 +``` + +`artifact_path` / `verify_artifact` 도 `ctx` 를 받도록 통일했다. Rev.1 의 +`(uuid, ws_key, home, claude_dir, iso_root)` 와 `(path, uuid, cwd)` 두 가지 관례가 +공존하던 것이 애초에 `cwd` 를 흘린 원인이다. + +--- + +## 4. M-1 — 우려는 옳고, 제안한 해법은 동작하지 않는다 + +### 4.1 우려: 실재한다 + +Rev.1 은 `PYTHONPATH` 를 `env_python` / `atomic_dump_yaml` 의 env 목록에만 얹었다. +그런데 `run_loop.sh` 는 **맨 `python3 -c` 를 5곳**(179, 210, 223, 250, 277) 쓴다. +그리고 Rev.1 §5 는 하필 그중 `resolve_agent_type`(223)을 `registry.agent_of_row` 로 +교체하라고 했다. 재현: + +``` +$ source .agents/skills/lib.sh; python3 -c "import mam_agents" +ModuleNotFoundError: No module named 'mam_agents' +``` + +Rev.1 설계 그대로 M1 을 구현했다면 `run_loop.sh` 가 그 자리에서 죽는다. + +### 4.2 제안: 순환이라 성립하지 않는다 + +`mam_agents/__init__.py` 안에서 `sys.path.insert` 를 하라는 제안은 실행될 수 없다. +`__init__.py` 가 돌려면 패키지가 이미 import 돼야 하고, import 되려면 경로가 이미 잡혀 있어야 한다. + +``` +$ python3 -c "import mam_agents" # sys.path 에서 skills 제거 후 +ModuleNotFoundError: No module named 'mam_agents' +-> __init__.py never runs, so it cannot add its own directory to sys.path +``` + +### 4.3 채택하는 해법: `lib.sh` source 시점 1회 export + +```bash +_mam_export_pythonpath() { + local d; d="$(mam_skills_dir)" + case ":${PYTHONPATH:-}:" in + *":$d:"*) ;; + *) export PYTHONPATH="$d${PYTHONPATH:+:$PYTHONPATH}" ;; + esac +} +_mam_export_pythonpath +``` + +`lib.sh` 를 source 하는 **모든** 스크립트의 **모든** 파이썬 호출이 한 번에 덮인다. +`run_loop.sh:12` 가 lib.sh 를 source 하므로 5곳 전부 포함된다. 검증: + +``` +$ source .agents/skills/lib.sh; python3 -c "from mam_agents import registry; print(registry.names())" +import OK: ['agy', 'claude', 'cline', 'hermes'] +``` + +**herdr shim 은 의도적으로 제외된다** — shim 은 lib.sh 를 source 하지 않는 별도 생성 스크립트이고, +Rev.1 §3.1 에서 그 안의 python3 9곳이 에이전트 지식을 0건 쓴다는 것을 이미 측정했다. + +**표준 라이브러리 섀도잉 위험 점검**: `.agents/skills/` 바로 아래에 최상위 `.py` 파일은 **0개**다 +(`mam_agents/` 패키지와 스킬 디렉터리뿐). export 후에도 stdlib import 정상: + +``` +$ source .agents/skills/lib.sh; python3 -c "import json, os, sqlite3, glob, re; print('stdlib OK')" +stdlib OK +``` + +> 남는 부작용 하나: herdr 가 띄우는 에이전트 CLI 들이 이 `PYTHONPATH` 를 상속한다. +> 최상위 모듈이 없어 섀도잉은 불가능하지만, 구현자는 `mam_agents` 라는 이름이 +> 어느 에이전트 CLI 의 내부 모듈과 겹치지 않는지 한 번 확인하는 편이 좋다. + +--- + +## 5. M-2 — 채택. 효과가 제언보다 크다 + +agy 는 "5번째 에이전트 추가 시 셸 수정 2~3곳 감소"로 추정했다. 실제로 세어 보니 +`_MAM_READY_TOKENS_CLAUDE` 는 **claude 전용 변수 하나**이고, 나머지 세 에이전트의 준비 토큰은 +`wait_for_tui_ready` 안에 **인라인으로 박혀 있다**(lib.sh:1811-1835). 그 case 블록이 **25줄**이다. + +``` +claude Anthropic|Assistant|Chat|Welcome +agy Antigravity +hermes Hermes +cline Cline|history|Chat|What can I do|slash commands +``` + +브리지가 `MAM_READY_TOKENS` 를 ERE alternation 으로 내보내면 25줄 case 가 +`grep -E -q "$MAM_READY_TOKENS"` 한 줄이 된다. 새 에이전트는 셸을 **0줄** 건드린다. + +> **행동 변경 주의.** claude 의 ready_tokens 에서 `projects` 를 **뺐다.** +> b107cf34 §2.7 에서 그 토큰이 cwd 경로에 우연히 매칭돼 **trust 다이얼로그가 떠 있는 상태에서 +> "준비 완료"로 오판**하는 것을 측정했기 때문이다. 이건 개선이지만 리팩터에 섞어 넣을 성질이 아니다. +> **별도 커밋으로 분리하고 자체 검증을 붙일 것을 권한다.** + +--- + +## 6. 변경 요약 (Rev.1 대비) + +| ID | 파일 | 내용 | +|---|---|---| +| R-1 | `base.py` | `DiscoveryContext` 도입, `discover()` 템플릿 메서드, `_raw_candidates()` 추상화, `_passes_epoch()` 훅, `ready_tokens` 속성 | +| R-2 | `adapters/agy.py` | `last_conversations.json[cwd]` 스코핑, 캐시 없으면 `[]`, 검증에 형제 점유 규칙 | +| R-3 | `adapters/hermes.py` | `WHERE cwd=?` 복원, `verify_artifact` 에 cwd 대조, `_passes_epoch` 를 `started_at` 으로 오버라이드 | +| R-4 | `adapters/cline.py` | 세션 json 의 `cwd` 로 원시 후보 스코핑 | +| R-5 | `adapters/claude.py` | `ctx` 서명 통일, `ready_tokens`(`projects` 제외) | +| R-6 | `lib.sh` | `PYTHONPATH` 를 source 시점 1회 export (per-entry-point env 목록 방식 폐기) | +| R-7 | `__main__.py` | 브리지에 `MAM_READY_TOKENS` 추가 | + +패키지 규모: Rev.1 374줄 → **Rev.2 484줄**. 증가분 110줄 대부분이 워크스페이스 스코핑과 +필터 템플릿이다. Rev.1 이 그만큼 덜 하고 있었다는 뜻이다. + +--- + +## 7. 검증 + +### 7.1 발견 정확도 — 어댑터 4종 + +워크스페이스 2개(`/work/mine`, `/work/other`), 세션 생성 epoch 1시간 전, +3개월 전 대화 1건, 형제가 점유한 id 1건을 심은 픽스처: + +| 어댑터 | Rev.1 | Rev.2 | 기대 | +|---|---|---|---| +| claude | — | `['cl-mine']` | ✅ | +| agy | `['agy-mine', 'agy-foreign']` | `['agy-mine']` | ✅ | +| hermes | `['herm-mine', 'herm-foreign', 'herm-ancient']` | `['herm-mine']` | ✅ | +| cline | — | `['cli-mine']` | ✅ | + +형제 점유 배제(전부 claimed 로 표시): + +``` +agy/claude/cline/hermes discover(all claimed) -> [] 4/4 OK +``` + +### 7.2 변이 — 6/6 검출 + +| 변이 | 되돌린 것 | 결과 | +|---|---|---| +| Q-1 | agy `_raw_candidates` → 플랫 glob (**Rev.1 그대로**) | `['agy-foreign', 'agy-mine']` WRONG | +| Q-2 | hermes `WHERE cwd=?` 제거 (**Rev.1 그대로**) | `herm-foreign` 유입 | +| Q-3 | hermes `_passes_epoch` 오버라이드 제거 | `herm-ancient` 유입 | +| Q-4 | 기반 클래스의 `claimed` 필터 제거 | 4종 전부 LEAKED | +| Q-5 | 기반 클래스의 `epoch` 필터 제거 | claude·cline·hermes 에 ancient 유입 | +| Q-6 | cline cwd 스코핑 제거 | `cli-foreign` 유입 | + +Q-1·Q-2 는 **Rev.1 코드를 그대로 변이로 삼은 것**이고 실제로 깨진다. +Q-3 은 §2.3 의 hermes 특수성이 공허한 우려가 아님을 보인다. + +### 7.3 회귀 + +``` +baseline (HEAD 245abe6) 162 passed in 518.51s +Rev.1 프로토타입 162 passed in 521.54s +Rev.2 프로토타입 162 passed in 505.79s ← 회귀 0 +``` + +R-6(source 시점 `PYTHONPATH` export)이 가장 위험했다. `lib.sh` 를 source 하는 모든 +스크립트의 환경을 바꾸고 herdr 가 띄우는 프로세스까지 상속되기 때문이다. 회귀 0. + +`py_compile` 통과. 어댑터는 표준 라이브러리만 사용(§Rev.1 3.2 제약 유지). + +--- + +## 8. 남는 위험 (Rev.1 §9 갱신) + +Rev.1 의 비용 항목 5가지(인터프리터 경계 · 브리지 호출 규율 · 배포/CI 등록 · 이행 중 이중 표현 · +간접화)는 그대로 유효하다. 아래는 갱신·추가분. + +**8.1 (갱신) 배포·CI 등록** — Rev.1 §8.2 의 `deploy/remove.sh` 한 줄과 §8.4 의 CI 경로 2줄은 +Rev.2 에서도 그대로 필수다. + +**8.2 (신규) hermes 스키마 미검증** — §2.3. `sessions(id, cwd, started_at)` 은 기존 쿼리와 +mock 에서 역산했다. hermes 미설치라 실 CLI 대조 불가. **M4 착수 전 확인 필요.** + +**8.3 (신규) agy 의 단일 방어선** — §3.2. agy 는 검증 단계에서 foreign 을 못 막는다(HEAD 동일). +캐시가 침묵하면 `[]` 를 반환하는 선택이 유일한 보호막이므로, 이 동작은 **테스트로 고정**해야 하고 +"후보가 안 잡힌다"는 버그 리포트가 올라올 때 되돌리고 싶어질 지점이다. 되돌리면 격리가 깨진다. + +**8.4 (신규) `projects` 토큰 제거는 행동 변경** — §5. 리팩터와 분리할 것. + +**8.5 (신규) `PYTHONPATH` 상속** — §4.3. 에이전트 CLI 들이 상속한다. 섀도잉 위험은 측정상 없으나 +이름 충돌 여부는 구현자가 확인. + +--- + +## 9. 이행 순서 (Rev.1 §10 갱신) + +| 단계 | 내용 | 변경점 | +|---|---|---| +| **M0** | 패키지 골격 + **source 시점 `PYTHONPATH` export**(R-6) + `deploy/remove.sh`·`install.sh`·CI 등록 | 부트스트랩 방식 교체 | +| **M1** | `own_key` / `agent_of_row` 이관 (프로토타입 완료, 34 → 29) | 변경 없음 | +| **M2** | `artifact_path` + `verify_artifact` — **`ctx` 서명으로 통일**, 격리 경로 일원화 | 서명 변경 | +| **M3** | `spawn_spec` / `resume_spec` / `auth_ok` | 변경 없음 | +| **M4** | `discover()` — drift-C 4블록. **hermes 스키마 확인이 선행**(§8.2) | 선행 조건 추가 | +| **M5** | `stop_session.sh` purge 경로 + exit key | 변경 없음 | +| **M6** | **(신규)** `ready_tokens` — `wait_for_tui_ready` 25줄 case 제거 | M-2 | +| **M7** | **(신규·별건)** claude ready token 에서 `projects` 제거 + 자체 검증 | §5 | + +중단 기준은 그대로: M2 이후 팬아웃이 29 → 20 이하로 안 떨어지면 재검토. + +--- + +## 10. 결론 + +이의 1건과 제언 2건 전부 채택했다. 그리고 셋 다 조사해 보니 지적된 것보다 컸다 — +C-1 은 서비스 거부가 아니라 **격리 위반**이었고, M-1 은 `run_loop.sh` 를 **죽이는** 문제였으며, +M-2 는 2~3곳이 아니라 **25줄 블록**이었다. + +그대로 채택하지 않은 것 하나. agy 의 권고는 `epoch`/`claimed` 를 **어댑터마다** 캡슐화하라는 것인데, +어댑터 4개가 각자 구현하면 잊어버릴 기회가 4번 생긴다. 방금 그 방식으로 두 개가 `cwd` 를 +잊은 것을 확인했다. 필터는 기반 클래스가 갖고, 어댑터는 스코프된 원시 후보만 낸다. + +프로토타입 트리: `scratchpad/ad2`(Rev.2) · `scratchpad/ad`(Rev.1) · `scratchpad/adbase`(HEAD). +`IMPROVEMENTS.md` A-4 항목은 Creator 구현 시 본 Rev.2 기준으로 갱신이 필요하다 — +이번 작업에서는 저장소를 건드리지 않았다. + +**[AGREEMENT: REACHED]** diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-0d9712c6.md b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-0d9712c6.md new file mode 100644 index 0000000..b595130 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-0d9712c6.md @@ -0,0 +1,82 @@ +# Cross-Code Review — Job `0d9712c6` + +- **Job ID**: 0d9712c6 · **Reviewer**: cline · **Base**: `245abe6` (working-tree, uncommitted) +- **Task**: `BaseAgentAdapter` (A-4) 아키텍처 설계를 `IMPROVEMENTS.md` 백로그에 등재한 누적 변경분에 대한 교차 코드 리뷰 (lint / 동작성 / 유실) +- **Diff scope**: `IMPROVEMENTS.md` 단일 파일 — `git diff --stat` = **1 file changed, 49 insertions(+), 3 deletions(-)** + +--- + +## 1. 변경분 요약 및 검증 대상 + +변경분은 코드가 아니라 **문서(백로그)**다. `IMPROVEMENTS.md` §1(아키텍처 결함)에 **A-4 (설계 제안): `BaseAgentAdapter` 어댑터 계층 도입 (Rev.2)** 항목을 신규 등재하고, 상단 집계(`11건 → 12건`, `아키텍처 1건 → 2건`)와 §1 제목 카운트(`1건 → 2건`), 최종 갱신일을 갱신했다. 저장소에 손댄 파일은 `IMPROVEMENTS.md` 하나뿐이다(`git status --porcelain`: ` M IMPROVEMENTS.md`). + +브리프에 포함된 diff 헤더와 실제 `git diff`는 정확히 일치한다. `mam_agents` 패키지/자산은 skills·deploy·tests 어디에도 존재하지 않음을 확인(`grep -rn mam_agents` 결과 0건, 보고서 제외) — 즉 이 변경은 순수 설계 기록이며 런타임 영향은 0이다. + +| 검증 항목 | 방법 | 결과 | +|---|---|---| +| Diff 일치 (브리프 vs working tree) | `git --no-pager diff IMPROVEMENTS.md` | ✅ 정확 일치 | +| 코드/자산 부재 확인 | `grep -rn mam_agents .agents/skills deploy tests` | ✅ 0건 (순수 문서) | +| 참조 프로토타입 보고서 존재 | `ls .mam/jobs/44062a63/claude-reports/report-final.md` | ✅ 존재 (18 KB) | + +--- + +## 2. Lint (정적 품질) + +`IMPROVEMENTS.md`는 Markdown 문서이므로 셸/파이썬 린트 대상이 아니다. Markdown 구조 정합성만 점검했다. + +- 헤더 계층(`#`/`##`/`###`/`####`) 일관, 테이블(`단계|내용`) 열 수 정합(2열), 인용 블록(`> 결함 조치가 아니라...`) 정상 종료. +- 인라인 코드 백틱 쌍 정합, 한국어/영문 혼용 깨짐 없음. +- 집계 숫자 변경(상단 `12건`/`아키텍처 2건` ↔ §1 제목 `2건`) 정합. `완료된 과제 10건` 줄과 기존 A-2 항목은 미변경(손대지 않음). + +**Lint 결과: PASS** — 구조적 결함 없음. + +--- + +## 3. 동작성 (설계 주장의 코드베이스 정합성) + +코드 변경이 없으므로, 설계 제안이 현 코드베이스 사실과 일치하는지(거짓 주장·과장 여부)를 검증했다. + +| 설계 주장 | 코드베이스 실측 | 판정 | +|---|---|---| +| `agent → *_id_own` 키 맵 **4벌** | 프로덕션 맵 3곳(`reconcile.sh:434`, `reconcile.sh:583`, `lib.sh:1393`) + 테스트 헬퍼 1곳(`conftest.py:262`) = 4 | ✅ 정합 | +| 세션명→에이전트 추론 **2벌**(규칙 상이) | `reconcile.sh:568 row_agent`(pane.cmd→cmd_full→접미사) vs `run_loop.sh:233-243`(세그먼트 매칭 + 실패 시 `claude` 기본값) | ✅ 정합 — 후자 오판 가능성 실재 확인 | +| `deploy/remove.sh:83-91` `fallback_assets` 미등록 | `remove.sh:83` `fallback_assets=(...)` 리스트 확인 — `.agents/skills/mam_agents` 누락 | ✅ 선행 체크리스트 #1 유효 | +| `tests/test_deploy_freshness.py::test_d2` 가드 | `test_d2_manifestless_removal_strands_no_framework_assets` 존재 | ✅ 선행 체크리스트 #1 근거 유효 | +| `gitea-ci.yml:69-77` flake8/py_compile 범위 제한 | `deploy/gitea-ci.yml:69,71,76` — `multi-agent-mux-delegate-job/scripts/` 한정 | ✅ 선행 체크리스트 #3 유효 | +| herdr shim `python3 -c` 9곳 에이전트 지식 0 | 본 리뷰 범위 외(프로토타입 실측)이나 참조 보고서 존재 | ⚠️ 미검증(프로토타입 영역) | +| 162 passed / 변이 6/6 / 배포 25/25 | 프로토타입 트리(저장소 미반영) — 재실행 불가 | ⚠️ 미검증(프로토타입 영역) | + +프로토타입 실측 수치(hermes shim, 162 passed 등)는 저장소에 반영되지 않은 scratchpad 결과이므로 본 리뷰에서 재검증할 수 없다. 다만 **저장소에 존재하는 사실**(키 맵 산재, 추론 2벌, fallback_assets/CI 범위)은 전부 정확히 확인됐다. 설계가 허위/과장에 기대지 않음. + +**동작성 결과: PASS** + +--- + +## 4. 유실 (Loss / Orphan) + +`git diff` 상 **삭제 3줄** 모두 교체성 갱신(최종 갱신일, 총 건수, §1 제목 카운트)이며 원 정보 손실 아님: +- `2026-08-08 (B-4 ...)` → `2026-08-09 (A-4 ... 등재)` : 갱신일 갱신(정당) +- `11건 (아키텍처 1건...)` → `12건 (아키텍처 2건...)` : 신규 항목 반영(정당) +- `Architecture Flaws — 1건` → `— 2건` : 항목 증가 반영(정당) + +기존 `A-2` 항목 본문, `완료된 과제 10건` 줄, §2~§6 섹션은 미변경(존재 보존 확인). 신규 자산(import/경로) 추가 없으므로 orphan 임포트/변수도 발생하지 않는다. + +**유실 결과: PASS** — 부당 삭제/잔재 없음. + +--- + +## 5. 비차단 발견 (Non-blocking Findings) + +**N-1 (참조 보고서 내 diff stat 불일치, 비본 diff).** 참조된 프로토타입 보고서(44062a63) 본문에 `git diff --stat: +44 / -3`로 기재됐으나, 실제 working-tree diff는 **+49 / -3**이다. 이는 *참조 보고서*의 기재 오류로, 본 리뷰 대상 diff(`IMPROVEMENTS.md`) 자체의 결함은 아니다. 비차단. + +**N-2 (섹션 명칭 vs 항목 성격).** §1 제목이 "아키텍처 **결함**"인데 A-4는 결함이 아닌 **설계 제안**이다. 등재자는 이를 인지하고 항목 제목에 `(설계 제안)`을 명시했으며, "아키텍처 과제"로의 개명은 문서 소유자 판단으로 남겨둠을 명시했다. 비차단 — 의도적 보존. + +**N-3 (프로토타입 수치 미검증).** "162 passed / 변이 6/6 / 배포 25/25" 및 hermes shim 9곳 지식-0 주장은 scratchpad 프로토타입 결과로, 저장소에 반영되지 않아 본 리뷰에서 재실행 불가. 설계 근거로서는 참조 보고서 존재로 충분하나, 정식 구현(M0~) 시점에 재측정이 권장됨. 비차단. + +--- + +## 6. 종합 판정 + +변경분은 `IMPROVEMENTS.md` 단일 문서에 대한 순수 추가적 설계 기록이다. 코드·배포 자산·테스트에 대한 변경이 전무하여 런타임·린트·회귀 영향은 0이다. 설계가 인용한 코드베이스 사실(키 맵 4벌, 추론 2벌 상이, fallback_assets/CI 범위 제한)은 실측 결과 전부 정확하며, 선행 필수 체크리스트 3항이 현 코드베이스의 실제 제약에 기반해 있다. 부당 삭제나 잔재도 없다. 단순 버그 수정 이상의 설계 재작업이 필요한 근거(escalation)는 발견되지 않는다 — 이 변경은 애초에 백로그 설계 제안 등재라는 명시적 산출물이며 그 목표를 충족한다. + +[VERDICT: PASS] \ No newline at end of file diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-3b42cc9b.md b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-3b42cc9b.md new file mode 100644 index 0000000..b8a03fe --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-3b42cc9b.md @@ -0,0 +1,90 @@ +# Cross-Code Review Report — Job 3b42cc9b + +- **Job ID**: 3b42cc9b +- **Reviewer**: cline (herdr:canary-projects-multi-agent-mux-creator-cline) +- **Base commit**: `245abe6` (working tree clean — diff reviewed: `9df0fc3..245abe6`) +- **Scope**: Audit `create_session.sh`, `reconcile.sh`, `resolve_session_id.sh`, `lib.sh` for 5 objectives. +- **Output**: `.mam/jobs/3b42cc9b/cline-reports/report-final.md` + +## 1. Audit Scope & Method + +The task is an **audit** of the current committed state of the four target scripts against five stated objectives: + +1. Sequential prompt injection +2. Post-spawn auto-pinning +3. Occupied-ID preemption guard +4. Stage 3 viewport verification +5. No UUID cross-talk or shadowing + +Method: read each target file end-to-end, trace each objective from creation → reconcile → resume, run `bash -n` (×4) + embedded-Python `compile()` (×8), then execute the three relevant test suites against the live tree. + +## 2. Lint & Test Results + +| Check | Result | +|---|---| +| `bash -n` `lib.sh` | PASS | +## 3. Objective-by-Objective Audit + +### 3.1 Sequential Prompt Injection — PASS + +`create_session.sh` enforces a strict spawn→ready→inject sequence: +- `spawn` (L165) → `wait_for_tui_ready` (L205, polls up to 30×1s for agent-specific ready tokens) → `handle_startup_dialogs` (claude only, L211) → pane meta capture → YAML append → **single** `inject_instructions` call (L376). +- No prompt is injected before the TUI is ready; only one prompt is injected per creation (no concurrent multi-prompt race). +- `inject_instructions` (lib.sh L1847) delegates to `send_keys_safe` (lib.sh L1932), which waits for `_pane_quiescent`, clears blocking dialogs (timeout-bounded), then atomically `set-buffer`/`paste-buffer`/`delete-buffer` + `C-m`. Submission is verified against rendered tokens (`●`, `✽`, `…ing`, `esc to interrupt`) over up to 3 retries. +- The `--submit-job` path publishes `started` **only after** injection returns rc 0 (L382); on failure it publishes `error` and exits 1 (L378-380). Sequential and ordered. + +### 3.2 Post-Spawn Auto-Pinning — PASS + +- `create_session.sh` (claude, L153/L157): `SESSION_UUID="$(mam_gen_uuid)"` → `CMD_FULL="... --session-id ${SESSION_UUID}"` → YAML stores `claude_session_id_own=assigned`, `session_id_source='assigned'`, `session_id_verified=False` (L320-323). +- `reconcile.sh` drift C0 confirms the assigned ID once the transcript materializes: `verify_session_uuid(mode="revalidate")` (lib.sh L1207-1209 shortcut returns True when workspace matches + source==assigned + verified==False, then the on-disk `.jsonl` check at L1212-1244 confirms it), after which `_pin_and_verify_resume` (reconcile.sh L432) sets `session_id_verified=True` and `last_visible_status='pinned'`. +- An immediate priority reconcile cycle is kicked off asynchronously right after creation (create_session.sh L384: `reconcile.sh --once &`), so pinning is attempted promptly without waiting for the next scheduled cycle. + +### 3.3 Occupied-ID Preemption Guard — PASS + +Four independent layers enforce that a fresh/resume session never gets an ID already occupied: +1. **Assign-time**: `mam_gen_uuid` generates a fresh random UUID (no reuse of existing). +2. **Resolve-time** (`find_workspace_uuid`, lib.sh L1406-1420): builds `running_ids` from ALL running sessions' own-IDs; `emit(u)` silently skips any UUID in `running_ids`. A resume will never be handed a live session's ID. +3. **Discover-time** (agy path, `verify_session_uuid` lib.sh L1263-1265): rejects a candidate present in `row['_sibling_claimed_uuids']` — collected in reconcile.sh L664-673 from sibling rows sharing the same cwd that are not stopped/terminated. +4. **Write-time** (validation layer, lib.sh L1083-1094): ID Uniqueness Check raises `SystemExit` if two running sessions share the same own-ID — defense-in-depth at persistence time. +| `bash -n` `create_session.sh` | PASS | +| `bash -n` `reconcile.sh` | PASS | +| `bash -n` `resolve_session_id.sh` | PASS | +| Embedded Python `compile()` (8 blocks across 5 files) | PASS | +| `tests/test_uuid_target.py` | **13/13 PASS** (53.43s) | +### 3.4 Stage 3 Viewport Verification — PASS + +`verify_tui_viewport` (lib.sh L1338-1363) implements the 3-stage viewport check: +- rc 2: session gone or pane capture empty/unavailable (degraded). +- rc 0: workspace `basename` (whitespace-stripped) appears in pane content (match). +- rc 1: a `/path/` pattern appears but the workspace basename does not (mismatch). + +`reconcile.sh` (all 4 agents, e.g. agy L686-695) gates pinning on this: with exactly one valid candidate, rc 0 → `_pin_and_verify_resume(degraded=False)`; rc 1 → `C-warn`, **not pinned** (will retry); rc 2 → `_pin_and_verify_resume(degraded=True)` (pin via stages 1-3 only, documented degraded path). Tests T-6 (degraded) and T-7 (mismatch) cover the non-happy paths. + +### 3.5 No UUID Cross-Talk or Shadowing — PASS + +- **Workspace scoping**: `verify_session_uuid` ORDERING INVARIANT (lib.sh L1199-1205) — the `workspace_key(cwd) != workspace_key(ws)` check runs BEFORE the assigned-id shortcut, so a row from a **different** workspace is rejected first even when assigned+unverified (tested T-12). `find_workspace_uuid` only considers sessions whose `pane.cwd == ws` (L1426). +- **C-ambiguous guard** (reconcile.sh, all 4 agents): when `len(valid_candidates) > 1`, reports `C-ambiguous` and does **not** pin (tested T-4) — no silent attribution of a possibly-wrong UUID. +- **Path canonicalization**: `mam_abs_workspace` uses `cd -P && pwd -P` (physical path) and `workspace_key` uses `os.path.realpath`. Shell (create/resolve) and Python (verify/find) therefore agree on the workspace key, preventing cross-talk from symlink/logical-path divergence (tested T-10 symlink + 6/6 path forms). +- `resolve_session_id.sh` (L44) is a thin wrapper over `find_workspace_uuid`, preserving the same workspace-isolated resolution path (P0-C: never returns a global id whose `project_cwd` differs from this workspace). +| `tests/test_o3_scoped_guard.py` + `test_sanity.py` + `test_b4_session_created.py` | **47/47 PASS** (18.03s) | +| `tests/test_tier3_integration.py::test_integration_stop_purge_combination` | **1/1 PASS** (33.26s) | +## 4. Findings (Non-Blocking) + +All findings are non-blocking; none require design rework. + +| # | Finding | Severity | Location | +|---|---|---|---| +| A-1 | **Wrapper-mode clears `SESSION_UUID` after `CMD_FULL` is composed.** In `spawn`'s claude wrapper branch (L170), `SESSION_UUID=""` is set *after* `CMD_FULL` already baked `--session-id ${SESSION_UUID}` (L157). The YAML `cmd_full` display field (L304) therefore records `--session-id ` even though the wrapper launch cleared it. The authoritative fields (`claude_session_id_own`, `session_id_verified`) are unaffected, so pinning/resume are correct; only the cosmetic `cmd_full` string is misleading. | Low / cosmetic | create_session.sh L157, L170, L304 | +| A-2 | **`verify_session_uuid` breaks on first cwd-bearing line.** In the claude branch (L1234-1236), the loop breaks as soon as a line carrying `cwd` is found, even if `sessionId` has not yet been confirmed on that line. If a transcript interleaves an unrelated `cwd` before the matching `sessionId` line, the function could `break` before `valid_session` is set. Safe-direction (fails closed — returns False rather than mis-attribute), so not a correctness bug, but worth a comment. | Low | lib.sh L1232-1236 | +| A-3 | **`mam_session_iso_root`/`mam_workspace_key` spawn Python per resume.** Each resume call re-launches a Python interpreter for these helpers, adding minor latency. No correctness impact. | Low / perf | lib.sh (helpers) | +| A-4 | **Drift-B `endswith` vs `row_agent()`.** The drift-C loops in `reconcile.sh` use the `row_agent()` helper, but the drift-B A-1 gate (the `endswith('-creator-')` inference in `find_workspace_uuid` L1473-1488) uses inline `endswith`. Different context (B is offline resolution; C is live reconcile), so not a bug, but a single-source consolidation would reduce drift. | Low / consistency | lib.sh L1473-1488 | + +No `[ESCALATE: PLANNER]` is warranted: every objective is satisfied and all findings are local fixes or cosmetic. + +## 5. Completeness & Loss Check + +- **Completeness**: all 5 objectives are implemented and exercised by `test_uuid_target.py` (13 cases) plus the cross-regression suite (47 cases). Every objective maps to at least one passing test (T-4 ambiguous, T-6/T-7 viewport, T-10 symlink, T-12 ordering invariant, T-5 custom name pinning). +- **Loss check**: the committed diff (`9df0fc3..245abe6`) adds `mam_gen_uuid`, `mam_abs_workspace`, `mam_workspace_key`, `mam_session_iso_root`, the C0/C-ambiguous/revalidate logic, and path canonicalization without removing prior resolution behavior for non-claude agents (agy/hermes/cline branches preserved and extended). No functional regression (47/47 + 1/1). +- **Working tree**: clean — no uncommitted changes outstanding for this scope. + +[VERDICT: PASS] \ No newline at end of file diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 3cd56a1..da41a28 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -1,8 +1,8 @@ # 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`) -- **최종 갱신일**: 2026-08-08 (B-4 시프트 ls created 포시스 타임스탬프 결함 조치 완료 반영) +- **최종 갱신일**: 2026-08-09 (A-4 `BaseAgentAdapter` 어댑터 계층 설계 제안 등재) - **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` -- **총 추적 미해결 과제**: **11건** (아키텍처 1건, 엣지케이스 6건, 오케스트레이션 1건, 레거시 잔재 3건) +- **총 추적 미해결 과제**: **12건** (아키텍처 2건, 엣지케이스 6건, 오케스트레이션 1건, 레거시 잔재 3건) - **완료된 과제**: **10건** (A-1, A-3, A-5, B-1, B-3, B-4, C-1, C-2, O-1, O-3) --- @@ -13,12 +13,58 @@ --- -## 1. 🔴 아키텍처 결함 (Architecture Flaws — 1건) +## 1. 🔴 아키텍처 결함 (Architecture Flaws — 2건) ### **A-2: 공개 브로커 + HMAC 인증 Off + 와일드카드 전파** - **현상**: `mqtt_common.py`의 기본 브로커가 공개 서버(`broker.hivemq.com`), HMAC 무조건 True 반환으로 설정되어 있습니다. - **파급 효과**: 외부에서 유입되는 malicious `error` 이벤트 수신 시 `reconcile.sh`가 라이브 에이전트 pane을 `kill-session`으로 강제 파괴하는 치명적 보안/안정성 위험이 존재합니다. +### **A-4 (설계 제안): 에이전트 지식 산재 — `BaseAgentAdapter` 어댑터 계층 도입 (Rev.2)** + +> 결함 조치가 아니라 **구조 개선 제안**입니다. 상세 설계·실측 근거는 `.mam/jobs/44062a63/claude-reports/report-final.md` 및 `744ac67a` 를 참조하십시오. + +- **현상**: "claude 의 transcript 는 어디 있나", "cline 재개 argv 는 무엇인가" 같은 **에이전트에 대한 사실**이 스크립트 8개 · **34개 팬아웃 지점**에 흩어져 있습니다. (8줄 윈도 안에 4개 에이전트 중 3개 이상이 등장하는 지점 기준: `create_session.sh` 8, `reconcile.sh` 7, `stop_session.sh` 6, `lib.sh` 4, `resolve_session_id.sh` 3, `resume_session.sh`/`update_yaml_resumed.sh`/`run_loop.sh` 각 2) +- **사본 현황**: `agent → *_id_own` 키 맵 **4벌**, `~/.claude/projects` 17참조, `antigravity-cli/conversations` 11참조, `.hermes/state.db` 6참조, `.cline/data/sessions` 5참조. +- **파급 효과**: 같은 질문에 **서로 다른 답**이 공존합니다. 세션명→에이전트 추론이 `reconcile.sh::row_agent`(pane.cmd → cmd_full → 이름 접미사)와 `run_loop.sh:236-243`(세그먼트 매칭 + 실패 시 `claude` 기본값) 두 벌로 존재하며 후자는 오판 가능합니다. b4a1d094 에서 드러난 "격리 분기 inert" 결함도 아티팩트 경로 규칙이 두 벌이었던 데서 비롯됐습니다. + +#### 설계 요지 (Rev.2 갱신) +- **`.agents/skills/mam_agents/`** 패키지(약 484줄): `base.py`(ABC + `SpawnSpec` + `DiscoveryContext`), `registry.py`(정적 레지스트리), `__main__.py`(셸 브리지), `adapters/{claude,agy,hermes,cline}.py`. +- **인터페이스**: `own_key` / `supports_assigned_id` / `ready_tokens` 속성 + `auth_ok(run)` · `spawn_spec()` · `resume_spec(binary, uuid, materialized)` · `artifact_path(uuid, ctx)` · `verify_artifact(uuid, ctx)` · `discover(ctx)`. + - **`DiscoveryContext` & `discover()` 템플릿 메서드**: `cwd`(물리 절대경로), `ws_key`, `home`, `claude_dir`, `iso_root`, `epoch`, `claimed` 필드를 객체 하나로 캡슐화. `_raw_candidates(ctx)` 추상 메서드로 어댑터별 물리 탐색만 서술하고 `claimed`/`epoch` 필터는 기반 클래스 템플릿이 100% 보장. + - `auth_ok` 는 서브프로세스 실행자를 **주입**받아 CLI 미설치 환경에서도 단위 테스트가 가능합니다(hermes 는 현재 미설치). + - `resume_spec` 의 `materialized` 파라미터가 b4a1d094 규칙(`claude -r <미실현 uuid>` 실패 → `--session-id`)을 흡수합니다. + - `ready_tokens` 속성을 어댑터로 내보내 `wait_for_tui_ready` 의 25줄 inline shell `case` 블록을 단 1줄의 `grep -E -q "$MAM_READY_TOKENS"` 로 축소. +- **레지스트리 패턴**: **정적 dict 채택.** `entry_points` 는 MAM 이 `pip install` 되지 않고 `rsync`/`cp` 로 배포되므로 부적합, 디렉터리 스캔은 "0개 발견"과 "에이전트 미설치"가 구분되지 않아 부적합. +- **bash↔python 브리지**: `python -m mam_agents facts ` 가 `shlex.quote` 된 `KEY=value` 를 출력. **스크립트당 1회 `eval`** 이 계약입니다(실측 22.8 ms/호출 vs bash `case` 2.3 ms — 분기마다 호출하면 안 됨). + +#### 실현 가능성 및 부트스트랩 (Rev.2 실측) +- 파이썬 진입점 **3종 전부**에서 import 확인: `env_python`, `atomic_dump_yaml`(flock 쓰기 경로), venv 없는 **맨 `/usr/bin/python3`**. +- **`lib.sh` source 시점 1회 `export PYTHONPATH`**: `lib.sh` 로드 시 `_mam_export_pythonpath` 가 `mam_skills_dir` 을 `PYTHONPATH` 에 자동 얹어 `run_loop.sh` 등 셸 상의 모든 `python3 -c` 호출이 한 번에 호환됨. +- herdr shim 내부 `python3 -c` **9곳 전부 에이전트 지식 0** → shim 경계가 이미 올바른 위치에 있습니다. +- **제약(규칙화 필요)**: 어댑터는 **표준 라이브러리만** 사용해야 합니다. 서드파티 의존성이 들어오면 venv 없는 경로가 깨집니다. + +#### 선행 필수 체크리스트 (누락 시 조용히 업그레이드가 막힘) +1. **`deploy/remove.sh:83-91` `fallback_assets` 에 `".agents/skills/mam_agents"` 등록.** + 미등록 시 `tests/test_deploy_freshness.py::test_d2` 가 실패하며(프로토타입에서 실제 발생), 언인스톨 후 잔존 자산이 워크스페이스를 **영구히 구버전에 고정**시킵니다(`install.sh` 는 비-스킬 자산을 "없을 때만" 복사). +2. `deploy/install.sh` 무결성 자산 목록 등록. +3. `deploy/gitea-ci.yml:69-77` 의 `flake8` / `py_compile` 경로 추가 — 현재 CI 파이썬 잡은 `multi-agent-mux-delegate-job/scripts/` 만 검사하므로 신규 패키지는 린트 사각지대입니다. + +#### 단계적 이행 (각 단계는 셸 사본 제거를 같은 커밋에 포함) +| 단계 | 내용 | +|---|---| +| M0 | 패키지 골격 + `lib.sh` source 시점 `PYTHONPATH` export + 배포/CI 등록 | +| M1 | `own_key` / `agent_of_row` 이관 — **프로토타입 검증 완료: 팬아웃 34 → 29** | +| M2 | `artifact_path` + `verify_artifact` (`verify_session_uuid` 4분기, `DiscoveryContext` 적용, 격리 경로 일원화) | +| M3 | `spawn_spec` / `resume_spec` / `auth_ok` (create·resume argv 및 프리플라이트) | +| M4 | `discover()` (raw candidates + base class template filter) — hermes DB 스키마 실측 선행 | +| M5 | `stop_session.sh` purge 경로 및 exit key | +| M6 | `ready_tokens` 어댑터 이관 (`wait_for_tui_ready` 25줄 case 제거) | +| M7 | claude ready tokens 에서 `projects` 제거 및 자체 검증 분리 커밋 | + +- **정직한 상한**: 34 → **약 10**. 남는 약 10곳은 셸 상주 TUI·프로세스 제어(`send_keys_safe` 의 agy/claude/cline 분기, `handle_startup_dialogs`, spawn 래퍼 분기, `pgrep -P` 자식 pid 게이트)로 이 추상화의 대상이 아닙니다. +- **중단 기준**: M2 이후에도 팬아웃이 29 → 20 이하로 떨어지지 않으면 중단하고 잔여 단계를 재검토합니다. +- **검증 상태**: Rev.2 프로토타입 기준 전체 회귀 **162 passed (회귀 0)**, 변이 6/6 검출, 배포 스위트 25/25, `py_compile` 통과. + --- ## 2. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 6건)