# Cross-Code Review: A-4 BaseAgentAdapter Introduction & Agent Knowledge Abstraction **Job ID**: 98e3986f **Reviewer**: cline **Date**: 2026-08-14 **Scope**: A-4 BaseAgentAdapter 도입 및 에이전트 지식 추상화 — lint, operability, and loss cross-review --- ## 1. Change Summary ### Modified Files (5) | File | Change | |------|--------| | `lib.sh:1120-1125` | F-1 fix: `VERIFY_SESSION_PYTHON` facade now fails explicitly (`echo ERROR + return/exit 1`) instead of silent empty string | | `lib_py/verify_session.py:82-86` | `home = home_dir or os.environ.get("HOME_DIR", "")` → `home = resolve_home(home_dir)` | | `lib_py/workspace_uuid.py:18-22` | `home = os.environ['HOME_DIR']` → `home = resolve_home()` | | `reconcile.sh:323,344-358,863-868` | Replaces `exec(os.environ['MAM_VERIFY_PY'])` with direct `from lib_py.verify_session import ...`; replaces subprocess `load_state_json` with `from lib_py.state import load_state_json` (with subprocess fallback); removes `MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON"` from both dry-run and write paths | | `deploy/gitea-ci.yml:76-78` | `py_compile` glob changed from flat `lib_py/*.py` to recursive `lib_py/**/*.py` to catch new subdirectories | ### New Files (13) | File | Purpose | |------|---------| | `lib_py/paths.py` | `resolve_home()` — unified HOME_DIR resolution contract (N0): explicit arg → HOME_DIR env → HOME env → expanduser('~'); raises ValueError on empty/root | | `lib_py/state.py` | `load_state_json()` — Python direct state loader for agent-sessions.yaml/.db (N7); faithful extraction of shell `load_state_json` | | `lib_py/agents/__init__.py` | Package init | | `lib_py/agents/base.py` | `BaseAgentAdapter` abstract base, `SpawnSpec`, `DiscoveryContext` (N4) | | `lib_py/agents/registry.py` | Static adapter registry: `get_adapter()`, `own_key()`, `agent_of_row()` (N4 & N5) | | `lib_py/agents/__main__.py` | CLI bridge: `python -m lib_py.agents ` | | `lib_py/agents/adapters/__init__.py` | Adapters package init | | `lib_py/agents/adapters/claude.py` | `ClaudeAgentAdapter` — `own_key = claude_session_id_own` | | `lib_py/agents/adapters/agy.py` | `AgyAgentAdapter` — `own_key = agy_conversation_id_own` | | `lib_py/agents/adapters/hermes.py` | `HermesAgentAdapter` — `own_key = hermes_conversation_id_own` | | `lib_py/agents/adapters/cline.py` | `ClineAgentAdapter` — `own_key = cline_conversation_id_own` | | `tests/test_a4_adapter_contract.py` | 3 contract tests: `resolve_home` contract, adapter registry, `agent_of_row` priority | --- ## 2. Syntax & Compilation Checks | Check | Result | |-------|--------| | `bash -n lib.sh` | ✅ PASS | | `bash -n reconcile.sh` | ✅ PASS | | `py_compile` all `lib_py/**/*.py` (recursive, 11 files) | ✅ PASS | | `py_compile tests/test_a4_adapter_contract.py` | ✅ PASS | --- ## 3. Import & Module Verification | Check | Result | |-------|--------| | `from lib_py.paths import resolve_home` | ✅ OK | | `from lib_py.state import load_state_json` | ✅ OK | | `from lib_py.agents.registry import get_adapter, own_key, agent_of_row` | ✅ OK | | `from lib_py.agents.base import BaseAgentAdapter, SpawnSpec, DiscoveryContext` | ✅ OK | | All 4 adapter imports (claude, agy, hermes, cline) | ✅ OK | | Circular import check | ✅ None — `base.py` → `paths`, `verify_session`; `registry.py` → `base` + adapters; adapters → `base` only | ### CLI Bridge Verification ``` $ python -m lib_py.agents facts claude AGENT_NAME=claude OWN_KEY=claude_session_id_own $ python -m lib_py.agents resolve my-workspace-creator-hermes hermes ``` ### Registry Edge Cases - `own_key('unknown')` → `None` ✅ - `own_key('')` → `None` ✅ - `get_adapter('CLAUDE')` → case-insensitive → `claude` ✅ --- ## 4. Test Results | Test Suite | Tests | Result | |------------|-------|--------| | `test_a4_adapter_contract.py` (new) | 3 | ✅ All PASS | | `test_sanity.py` | 2 | ✅ All PASS | | `test_tier1_unit.py` | 29 | ✅ All PASS | | `test_orc_onboard.py` | 40 | ✅ All PASS | | **Total** | **74** | **✅ All PASS** | New contract tests verify: 1. `resolve_home` contract: explicit arg, HOME_DIR env, HOME/expanduser fallback 2. Adapter registry: all 4 agents have correct `name` and `own_key` (claude → `session`, others → `conversation`) 3. `agent_of_row` priority: explicit `agent` field > session name suffix > pane.cmd match --- ## 5. Design Analysis ### 5.1 `resolve_home()` Contract (N0) Well-designed unified home resolution with fail-close semantics: - **Priority chain**: explicit arg → `HOME_DIR` env → `HOME` env → `expanduser('~')` - **Fail-close**: raises `ValueError` if result is empty or `/` — prevents root-relative silent fail-close - **Behavioral change in `verify_session.py`**: Old code `home_dir or os.environ.get("HOME_DIR", "")` silently returned `""` → now raises `ValueError`. This is the intended fix — callers that previously got `""` would build paths like `/.claude/projects` (root-relative), which is a silent failure mode. - **Behavioral improvement in `workspace_uuid.py`**: Old code `os.environ['HOME_DIR']` raised `KeyError` on missing env → now falls back to `HOME`/`expanduser`. More robust. ### 5.2 BaseAgentAdapter & Registry (N4 & N5) Clean OOP design: - `BaseAgentAdapter` defines `name`, `own_key` (abstract properties), `derive_session_name()`, `matches_session_name()`, `verify_session()` - `matches_session_name()` checks `-{role}-{name}` suffix (creator/planner/reviewer) or bare `-{name}` — matches the kind detection logic in `lib.sh` - `derive_session_name()` returns `f"{slug}-{role}-{name}"` — matches the shell `derive_session_name` function - `agent_of_row()` implements strict priority: explicit `agent` field → session name suffix → `pane.cmd` exact/binary-path match → None - Static registry with 4 singleton adapter instances — no dynamic registration needed ### 5.3 `state.py` Extraction (N7) Faithful extraction of shell `load_state_json` (lib.sh:828-869): - Identical DB-first, YAML-fallback logic - Identical `clean_surrogates` recursive cleaner - Identical `sqlite3.connect(timeout=60.0)` + `PRAGMA busy_timeout = 60000` - **Enhancement**: accepts `yaml_path` parameter with env var fallback (`AGENT_SESSIONS_YAML` or `YAML_PATH`) — more flexible than shell version which always gets `YAML_PATH` from `env_python` - **Difference**: returns dict directly instead of printing JSON to stdout — correct for in-process use ### 5.4 `reconcile.sh` Decoupling - `exec(os.environ['MAM_VERIFY_PY'])` → `from lib_py.verify_session import verify_session_uuid, workspace_key` — eliminates the env-var code injection pattern - `MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON"` removed from both dry-run and write paths — no more env-var coupling - `load_state_json` now tries direct Python import first, falls back to subprocess — performance improvement with safe fallback ### 5.5 CI glob fix (gitea-ci.yml) Old: `py_compile .agents/skills/lib_py/*.py` (flat — misses `lib_py/agents/**/*.py`) New: `glob.glob('.agents/skills/lib_py/**/*.py', recursive=True)` — correctly catches all nested Python files. **Necessary change.** --- ## 6. Findings ### F-1 (Low): Unused imports in `base.py` and `__main__.py` - `base.py:3`: `import os, json, sqlite3` — none used in the file body - `base.py:4`: `from typing import Optional, Dict, Any, List` — `List` unused - `__main__.py:3`: `import sys, json` — `json` unused - **Impact**: Dead imports; would be flagged by flake8 F401. No runtime impact. - **Recommendation**: Remove unused imports for cleanliness. ### F-2 (Low): `VERIFY_SESSION_PYTHON` is now a dead variable - `lib.sh:1121` defines `VERIFY_SESSION_PYTHON="$(cat ...)"` but no consumer remains — `reconcile.sh` was the only consumer (via `MAM_VERIFY_PY`), now removed. - The `if-else` block still serves as a **file existence guard** (else branch fails explicitly), but the variable assignment itself is dead code. - **Impact**: None at runtime — the guard is useful, the variable is harmless dead code. - **Recommendation**: Could simplify to a pure existence check, but keeping it documents the historical facade pattern. Acceptable as-is. ### F-3 (Low): `reconcile.sh:326` still uses `os.environ['HOME_DIR']` directly - Line 326: `home = os.environ['HOME_DIR']` — not changed to `resolve_home()` - `reconcile.sh` always sets `HOME_DIR="$HOME_DIR"` via shell wrapper (line 71), so `KeyError` is impossible in practice - **Impact**: Inconsistency with the `resolve_home()` pattern adopted in `verify_session.py` and `workspace_uuid.py`. No runtime risk. - **Recommendation**: Could adopt `resolve_home()` for consistency, but not required since the env var is always set by the wrapper. ### F-4 (Low): `test_resolve_home_contract` test 3 fragility - Test 3 creates `env_copy` with `HOME_DIR` popped, computes expected `home_val` from the copy, but calls `resolve_home()` with the **actual** `os.environ` (which may still have `HOME_DIR` set from prior tests or environment). - If `HOME_DIR` was set in the environment when the test runs, `resolve_home()` would return the `HOME_DIR` value, but `home_val` would be `HOME`/`expanduser` — mismatch → test failure. - **Impact**: Test passes in current environment (HOME_DIR not set during direct pytest run), but is fragile in environments where HOME_DIR is pre-set. - **Recommendation**: Use `monkeypatch.delenv('HOME_DIR', raising=False)` to properly isolate the test. --- ## 7. Loss / Regression Analysis | Concern | Status | |---------|--------| | `MAM_VERIFY_PY` references in codebase | ✅ Fully removed from all `.sh`/`.py` files (only in `.mam/jobs/` probe scripts — historical) | | `load_state_json` shell function still used by other scripts | ✅ Yes — `stop_session.sh`, `orc_onboard.sh`, `run_loop.sh`, `lib.sh` itself. The new `state.py` module coexists; only `reconcile.sh` uses the Python import path. No loss. | | `conftest.py` copies `lib_py/` to test sandboxes | ✅ `shutil.copytree(src_skills, ...)` copies entire `.agents/skills/` tree including new `lib_py/agents/` subdirectory | | `deploy/remove.sh` includes new files | ✅ `.agents/skills/lib_py` directory entry covers `agents/` subdirectory automatically | | `derive_session_name` logic matches shell version | ✅ `f"{slug}-{role.lower()}-{name}"` — identical | | `matches_session_name` logic matches shell kind detection | ✅ Checks `-{role}-{name}` and `-{name}` suffixes — consistent | | `own_key` naming convention | ✅ claude → `session_id_own`, others → `conversation_id_own` — matches existing schema | | F-1 fix from prior review (job 11a99829) correctly applied | ✅ `return 1 2>/dev/null || exit 1` handles both sourced and executed contexts | --- ## 8. Verdict The A-4 BaseAgentAdapter introduction and agent knowledge abstraction changes are **well-designed, correctly implemented, and fully tested**: - **No regressions**: All 74 tests pass (including 3 new contract tests) - **No code loss**: All existing consumers of `load_state_json` and `verify_session_uuid` continue to work; new Python module coexists with shell versions - **Clean design**: `BaseAgentAdapter` + static registry + per-agent adapters follows OOP best practices; `resolve_home()` contract prevents root-relative silent fail-close - **Proper decoupling**: `reconcile.sh` eliminates env-var code injection (`exec(os.environ['MAM_VERIFY_PY'])`) in favor of direct imports - **CI coverage**: Recursive glob fix ensures new nested Python files are compiled in CI The 4 findings (F-1 through F-4) are all **Low severity** — unused imports, dead variable, consistency gap, and test fragility. None block the PASS verdict. [VERDICT: PASS]