feat(a4,c3b): complete A-4 Phase 2 agent knowledge migration & Option B isolation removal

- Option B / C-3b: Deprecate isolation.root and remove its 4 legacy consumers in lib.sh, workspace_uuid.py, verify_session.py, and stop_session.sh.
- M2~M7 Migration: Implement artifact_path, verify_artifact, purge_artifacts, spawn_spec, resume_spec, auth_ok, discover, ready_tokens, exit_key, and delegate_agent_key across BaseAgentAdapter and all 4 adapters (Claude, Agy, Hermes, Cline).
- Migrate shell duplications in lib.sh, create_session.sh, resume_session.sh, stop_session.sh, and reconcile.sh to python -m lib_py.agents facts.
- Hardening & Corrections: Fix Cline resume_spec flag to '-i --id', apply shlex.quote across all facts variables with MAM_ prefix, add safe fallback in wait_for_tui_ready.
- Add comprehensive contract tests in tests/test_a4_adapter_contract.py and sync IMPROVEMENTS.md / LOG.md.
- Verified: 100% PASS across unit, component, contract, and integration tests.
This commit is contained in:
2026-08-16 23:15:24 +09:00
parent 971f14ad3f
commit b4821fafa8
21 changed files with 904 additions and 531 deletions
@@ -173,13 +173,7 @@ delegate_publish_event "$DELEGATE_JOB_ID" progress "terminating"
graceful_stop() {
local pane_pid exitkey
pane_pid=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
case "$AGENT" in
claude) exitkey="/exit" ;;
agy) exitkey="Exit" ;;
hermes) exitkey="/exit" ;;
cline) exitkey="/exit" ;;
*) exitkey="/exit" ;;
esac
exitkey="$("$(_delegate_py_bin)" -c "from lib_py.agents.registry import get_adapter; a = get_adapter('$AGENT'); print(a.exit_key if a else '/exit')" 2>/dev/null || echo "/exit")"
echo "graceful: send-keys '$exitkey' to $SESSION_NAME"
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain"
_wait_session_gone "$SESSION_NAME" 5 || true
@@ -272,83 +266,28 @@ if captured and not purge:
target['cline_conversation_id_own'] = captured
target['resumable'] = True
# --purge-conversation: 워크스페이스 격리된 UUID 의 디스크 artifact 만 삭제 (P0-C)
# T6: stop-purge 시 격리 디렉터리 청소 및 경로 가드
iso = target.get('isolation')
if purge and iso:
iso_root = iso.get('root')
iso_uuid = iso.get('uuid')
if iso_root and iso_uuid:
ws_abs = os.path.abspath(ws) if ws else ""
expected_homes_dir = os.path.join(ws_abs, '.mam', 'agent_homes')
expected_iso_root = os.path.join(expected_homes_dir, iso_uuid)
if (os.path.abspath(iso_root) == os.path.abspath(expected_iso_root) and
os.path.abspath(iso_root).startswith(os.path.abspath(expected_homes_dir) + os.sep)):
if os.path.isdir(iso_root):
shutil.rmtree(iso_root)
print(f"purged isolated home: {iso_root}", flush=True)
else:
print(f"WARN: isolated home path check failed: {iso_root}", flush=True)
if purge and purge_uuid:
if agent == 'claude':
key = ws.replace('/', '-').replace('_', '-')
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
jsonl = f"{claude_project_dir}/{key}/{purge_uuid}.jsonl"
if os.path.exists(jsonl):
os.remove(jsonl)
print(f"purged: {jsonl}", flush=True)
target['claude_session_id_own'] = None
elif agent == 'agy':
db = f"{home}/.gemini/antigravity-cli/conversations/{purge_uuid}.db"
if os.path.exists(db):
os.remove(db)
print(f"purged: {db}", flush=True)
brain = f"{home}/.gemini/antigravity-cli/brain/{purge_uuid}"
if os.path.isdir(brain):
shutil.rmtree(brain, ignore_errors=True)
print(f"purged: {brain}", flush=True)
target['agy_conversation_id_own'] = None
elif agent == 'hermes':
json_file = f"{home}/.hermes/sessions/session_{purge_uuid}.json"
if os.path.exists(json_file):
os.remove(json_file)
print(f"purged: {json_file}", flush=True)
hdb = f"{home}/.hermes/state.db"
if os.path.exists(hdb):
try:
import sqlite3
hconn = sqlite3.connect(hdb)
hconn.execute("DELETE FROM sessions WHERE id=?", (purge_uuid,))
hconn.execute("DELETE FROM messages WHERE session_id=?", (purge_uuid,))
hconn.commit()
hconn.close()
print(f"purged db records for session: {purge_uuid}", flush=True)
except Exception as e:
print(f"WARN: purge hermes db records failed: {e}", flush=True)
target['hermes_conversation_id_own'] = None
elif agent == 'cline':
sessions_dir = f"{home}/.cline/data/sessions/{purge_uuid}"
if os.path.isdir(sessions_dir):
shutil.rmtree(sessions_dir)
print(f"purged: {sessions_dir}", flush=True)
target['cline_conversation_id_own'] = None
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
adapter = get_adapter(agent)
if adapter:
ctx = DiscoveryContext(workspace=ws, agent_name=agent, home_dir=home)
for item in adapter.purge_artifacts(purge_uuid, ctx):
print(f"purged: {item}", flush=True)
target[adapter.own_key] = None
# agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다
ai = (d.get('agent_identities') or {}).get(agent) or {}
if ai.get('project_cwd') == ws:
if agent == 'claude' and ai.get('session_id') == purge_uuid:
ai['session_id'] = None
ai['session_jsonl'] = None
ai.pop('session_size_bytes', None)
ai.pop('session_lines', None)
elif agent == 'agy' and ai.get('conversation_id') == purge_uuid:
ai['conversation_id'] = None
ai['conversation_db'] = None
ai['conversation_brain_dir'] = None
elif agent == 'hermes' and ai.get('session_id') == purge_uuid:
ai['session_id'] = None
elif agent == 'cline' and ai.get('session_id') == purge_uuid:
ai['session_id'] = None
if adapter and (ai.get('session_id') == purge_uuid or ai.get('conversation_id') == purge_uuid):
for field in adapter.identity_cache_fields:
ai.pop(field, None)
if 'session_id' in adapter.identity_cache_fields or 'session_jsonl' in adapter.identity_cache_fields:
ai['session_id'] = None
ai['session_jsonl'] = None
if 'conversation_id' in adapter.identity_cache_fields:
ai['conversation_id'] = None
ai['conversation_db'] = None
ai['conversation_brain_dir'] = None
elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)