feat(opencode): wire 29-point CLI lifecycle scripts, drift-C reconciler, and skill docs
This commit is contained in:
@@ -317,11 +317,13 @@ fi
|
||||
# atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는
|
||||
# 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지).
|
||||
read -r -d '' RECON_SRC <<'PYEOF' || true
|
||||
import os, json, glob, subprocess, time, sqlite3, re
|
||||
import os, json, glob, subprocess, time, sqlite3, re, shutil
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
from lib_py.verify_session import verify_session_uuid, workspace_key
|
||||
from lib_py.agents.registry import get_adapter
|
||||
from lib_py.agents.base import DiscoveryContext
|
||||
|
||||
def _slug(path):
|
||||
if not path:
|
||||
@@ -518,7 +520,7 @@ if herdr_confirmed:
|
||||
agent = None
|
||||
role = 'creator'
|
||||
for r_name in ('creator', 'planner', 'reviewer'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
if name.endswith(f"-{r_name}-{a_name}"):
|
||||
role = r_name
|
||||
agent = a_name
|
||||
@@ -537,7 +539,7 @@ if herdr_confirmed:
|
||||
if tok.startswith('MAM_MANAGED='):
|
||||
managed_path = tok.split('=', 1)[1]
|
||||
if os.path.realpath(managed_path) == os.path.realpath(workspace_root):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
|
||||
agent = a_name
|
||||
break
|
||||
@@ -598,6 +600,9 @@ if herdr_confirmed:
|
||||
elif agent == 'hermes':
|
||||
entry['child_pid'] = 0
|
||||
entry['hermes_conversation_id_own'] = None
|
||||
elif agent == 'opencode':
|
||||
entry['child_pid'] = 0
|
||||
entry['opencode_session_id_own'] = None
|
||||
d.setdefault('herdr_sessions', []).append(entry)
|
||||
yaml_session_names.add(name)
|
||||
drifts.append({'class': 'B', 'name': name,
|
||||
@@ -608,7 +613,7 @@ def row_agent(s):
|
||||
return agent_of_row(s)
|
||||
|
||||
OWN_KEY_BY_AGENT = {
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok')
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')
|
||||
}
|
||||
|
||||
# === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 ===
|
||||
@@ -779,6 +784,118 @@ for s in d.get('herdr_sessions', []):
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'hermes', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift C (opencode): opencode 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if row_agent(s) != 'opencode':
|
||||
continue
|
||||
if s.get('status') != 'running':
|
||||
continue
|
||||
if s.get('opencode_session_id_own'):
|
||||
continue
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
|
||||
# Resolve DB path via shared OpenCodeAgentAdapter helper with fallbacks
|
||||
odb = None
|
||||
try:
|
||||
op_adapter = get_adapter('opencode')
|
||||
if op_adapter:
|
||||
odb = op_adapter._resolve_db(DiscoveryContext(workspace=cwd, agent_name='opencode', home_dir=home))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not odb:
|
||||
opencode_base = f"{home}/.local/share/opencode"
|
||||
candidates = [
|
||||
f"{opencode_base}/opencode.db",
|
||||
f"{opencode_base}/storage.db",
|
||||
]
|
||||
for cand in candidates:
|
||||
if os.path.exists(cand):
|
||||
odb = cand
|
||||
break
|
||||
if not odb and os.path.isdir(f"{opencode_base}/project"):
|
||||
for root, _, files in os.walk(f"{opencode_base}/project"):
|
||||
for f in files:
|
||||
if f.endswith(".db"):
|
||||
odb = os.path.join(root, f)
|
||||
break
|
||||
if odb and os.path.exists(odb):
|
||||
break
|
||||
|
||||
if not odb or not os.path.exists(odb):
|
||||
continue
|
||||
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
|
||||
sibling_claimed = [
|
||||
other.get('opencode_session_id_own')
|
||||
for other in d.get('herdr_sessions', [])
|
||||
if other is not s
|
||||
and (other.get('pane') or {}).get('cwd') == cwd
|
||||
and other.get('status') not in ('stopped', 'terminated')
|
||||
and other.get('opencode_session_id_own')
|
||||
]
|
||||
s_eval = dict(s)
|
||||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
conn = sqlite3.connect(odb)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("PRAGMA table_info(session)")
|
||||
cols = [col[1] for col in cursor.fetchall()]
|
||||
if cols:
|
||||
cwd_col = "directory" if "directory" in cols else ("cwd" if "cwd" in cols else None)
|
||||
time_col = "time_created" if "time_created" in cols else ("created_at" if "created_at" in cols else ("started_at" if "started_at" in cols else None))
|
||||
|
||||
query = "SELECT id FROM session"
|
||||
params = []
|
||||
where_clauses = []
|
||||
if cwd_col and cwd:
|
||||
where_clauses.append(f"{cwd_col} = ?")
|
||||
params.append(cwd)
|
||||
if time_col and epoch_threshold:
|
||||
if time_col == "time_created":
|
||||
where_clauses.append(f"{time_col} >= ?")
|
||||
params.append(int(float(epoch_threshold) * 1000))
|
||||
else:
|
||||
where_clauses.append(f"{time_col} >= ?")
|
||||
params.append(epoch_threshold)
|
||||
if where_clauses:
|
||||
query += " WHERE " + " AND ".join(where_clauses)
|
||||
if time_col:
|
||||
query += f" ORDER BY {time_col} DESC"
|
||||
query += " LIMIT 20"
|
||||
rows = cursor.execute(query, params).fetchall()
|
||||
conn.close()
|
||||
for (uuid,) in rows:
|
||||
if uuid in (sibling_claimed or []):
|
||||
continue
|
||||
if verify_session_uuid(cwd, 'opencode', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
else:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(valid_candidates) > 1:
|
||||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||||
s['last_visible_status'] = f"ambiguous: {len(valid_candidates)} candidates"
|
||||
actions.append(f"ambiguous candidates: {s['name']}")
|
||||
if len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "opencode" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
|
||||
|
||||
result = {
|
||||
'timestamp': now_iso,
|
||||
'yaml_path': yaml_path,
|
||||
|
||||
Reference in New Issue
Block a user