From 94af7f6b8ae3ae10bbc512239cb57a3f461bd7c2 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sat, 29 Aug 2026 14:59:01 +0900 Subject: [PATCH] feat(opencode): wire 29-point CLI lifecycle scripts, drift-C reconciler, and skill docs --- .agents/skills/lib.sh | 6 +- .../skills/multi-agent-mux-create/SKILL.md | 7 +- .../scripts/create_session.sh | 35 ++++- .../multi-agent-mux-loop/scripts/run_loop.sh | 4 + .../scripts/reconcile.sh | 125 +++++++++++++++++- .../multi-agent-mux-orc-onboard/SKILL.md | 5 +- .../scripts/orc_onboard.sh | 6 +- .../skills/multi-agent-mux-resume/SKILL.md | 4 +- .../scripts/resolve_session_id.sh | 8 +- .../scripts/resume_session.sh | 11 +- .../scripts/update_yaml_resumed.sh | 18 ++- .../multi-agent-mux-status/scripts/status.sh | 97 +++++++++++++- .agents/skills/multi-agent-mux-stop/SKILL.md | 2 +- .../scripts/stop_session.sh | 10 +- 14 files changed, 303 insertions(+), 35 deletions(-) diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 842fa9e..40a2106 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -607,15 +607,18 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens)) *-creator-agy|*-planner-agy|*-reviewer-agy) kind="agy" ;; *-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;; *-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok" ;; + *-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode" ;; *) if echo "$name" | grep -qi "agy"; then kind="agy" elif echo "$name" | grep -qi "claude"; then kind="claude" elif echo "$name" | grep -qi "hermes"; then kind="hermes" elif echo "$name" | grep -qi "grok"; then kind="grok" + elif echo "$name" | grep -qi "opencode"; then kind="opencode" elif echo "${final_cmd:-}" | grep -qi "agy"; then kind="agy" elif echo "${final_cmd:-}" | grep -qi "claude"; then kind="claude" elif echo "${final_cmd:-}" | grep -qi "hermes"; then kind="hermes" elif echo "${final_cmd:-}" | grep -qi "grok"; then kind="grok" + elif echo "${final_cmd:-}" | grep -qi "opencode"; then kind="opencode" fi ;; esac @@ -629,7 +632,7 @@ try: tokens = shlex.split(cmd) if tokens: first = tokens[0] - if first in ('claude', 'agy', 'hermes', 'grok') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok')) or (kind and (first == kind or first.endswith('/' + kind))): + if first in ('claude', 'agy', 'hermes', 'grok', 'opencode') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')) or (kind and (first == kind or first.endswith('/' + kind))): tokens = tokens[1:] print(' '.join(shlex.quote(t) for t in tokens)) except Exception: @@ -2059,6 +2062,7 @@ send_keys_safe() { *-agy|*-agy-[0-9]*|agy|agy-[0-9]*) _sks_agent="agy" ;; *-hermes|*-hermes-[0-9]*|hermes|hermes-[0-9]*) _sks_agent="hermes" ;; *-grok|*-grok-[0-9]*|grok|grok-[0-9]*) _sks_agent="grok" ;; + *-opencode|*-opencode-[0-9]*|opencode|opencode-[0-9]*) _sks_agent="opencode" ;; esac if [ -n "$_sks_agent" ]; then eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$_sks_agent" 2>/dev/null || true)" diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index 97013a1..95b3473 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -129,7 +129,7 @@ herdr_sessions: ```bash WORKSPACE=/path/to/project -AGENT=claude # claude | agy | hermes | grok — always pass it explicitly +AGENT=claude # claude | agy | hermes | grok | opencode — always pass it explicitly source .agents/skills/lib.sh SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")" @@ -160,7 +160,10 @@ case "$AGENT" in grok) herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions" ;; - *) echo "ERROR: --agent must be claude, agy, hermes, or grok, got: $AGENT"; exit 2 ;; + opencode) + herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "opencode --auto --agent build" + ;; + *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT"; exit 2 ;; esac # 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s) diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index e8867c5..5441764 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # create_session.sh — multi-agent-mux-create 의 부속 스크립트 # Usage: -# bash create_session.sh --workspace --agent --role [--session ] [--herdr-session ] [--wrapper] +# bash create_session.sh --workspace --agent --role [--session ] [--herdr-session ] [--wrapper] # # 동작: # 1) preflight: herdr/claude/agy 가용성, workspace 존재 @@ -26,11 +26,11 @@ source "$_lib_sh" usage() { cat < --agent --role [options] +Usage: $0 --workspace --agent --role [options] Options: --workspace PATH project directory (required) - --agent AGENT claude | agy | hermes | grok (required) + --agent AGENT claude | agy | hermes | grok | opencode (required) --role ROLE assigned role (required) --session NAME herdr session name (default: derived from workspace) --wrapper force use of ~/.local/bin/ wrapper even if not present @@ -90,8 +90,8 @@ fi [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; } case "$AGENT" in - claude|agy|hermes|grok) ;; - *) echo "ERROR: --agent must be claude, agy, hermes or grok, got: $AGENT" >&2; exit 2 ;; + claude|agy|hermes|grok|opencode) ;; + *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;; esac [ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; } [ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; } @@ -126,6 +126,13 @@ elif [ "$AGENT" = "grok" ]; then echo "ERROR: grok is not functional or configured." >&2 exit 1 fi +elif [ "$AGENT" = "opencode" ]; then + if [ -f "$HOME/.local/share/opencode/auth.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ] || [ -n "${GEMINI_API_KEY:-}" ] || [ -n "${GROQ_API_KEY:-}" ]; then + true + elif ! opencode --version >/dev/null 2>&1; then + echo "ERROR: opencode is not functional or configured." >&2 + exit 1 + fi fi # 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A) @@ -181,6 +188,8 @@ if [ -z "$CMD_FULL" ]; then claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;; agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;; hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;; + grok) CMD_FULL="${RESOLVED_BIN} --permission-mode bypassPermissions" ;; + opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;; esac fi @@ -204,7 +213,13 @@ spawn() { agy|hermes|grok) HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" ;; - *) echo "ERROR: --agent must be claude, agy, hermes or grok, got: $AGENT" >&2; exit 2 ;; + opencode) + if [ -z "${OPENCODE_PERMISSION:-}" ]; then + export OPENCODE_PERMISSION='{"*":"allow"}' + fi + HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" + ;; + *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;; esac } @@ -312,6 +327,7 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then hermes) delegate_agent="hermes-agent" ;; agy) delegate_agent="antigravity-cli" ;; grok) delegate_agent="grok-build" ;; + opencode) delegate_agent="opencode-cli" ;; *) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;; esac fi @@ -419,6 +435,13 @@ elif agent == 'grok': entry['session_id_source'] = 'assigned' if assigned else 'pending-discovery' entry['session_id_verified'] = False entry['last_visible_status'] = "assigned (awaiting first message)" if assigned else "unverified" +elif agent == 'opencode': + cp = os.environ.get('CHILD_PID', '0') + entry['child_pid'] = int(cp) if cp.isdigit() else 0 + entry['opencode_session_id_own'] = None + entry['session_id_source'] = 'pending-discovery' + entry['session_id_verified'] = False + entry['last_visible_status'] = "unverified" last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '') if last_vis_override: diff --git a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh index 1eedc95..ac9ae89 100644 --- a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh @@ -309,6 +309,10 @@ if not agent: agent = 'agy' elif 'hermes' in segments: agent = 'hermes' + elif 'grok' in segments: + agent = 'grok' + elif 'opencode' in segments: + agent = 'opencode' else: agent = 'claude' print(agent) diff --git a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh index 1fe5e2f..53586b5 100755 --- a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh +++ b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh @@ -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, diff --git a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md index 640eed5..bb9ee82 100644 --- a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md +++ b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos] environments: [terminal, herdr] metadata: hermes: - tags: [agent, herdr, claude, antigravity, agy, hermes, grok, orchestrator, onboard, isolation] + tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, orchestrator, onboard, isolation] related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-monitor] prereq_skills: [multi-agent-mux-create] --- @@ -32,12 +32,13 @@ Registers an orchestrator session UUID into the `orchestrator_uuids` list of `.m ## Auto-Detection Hierarchy (Rev.2) -When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`) in the following order: +When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`, `opencode`) in the following order: 1. **CLI `argv`**: - `claude -r ` / `claude --session-id ` - `agy --conversation ` - `grok --session-id ` / `grok --resume ` + - `opencode --session ` 2. **Family-Matched Environment Variables**: - `claude` → `CLAUDE_CODE_SESSION_ID` - `agy` → `ANTIGRAVITY_CONVERSATION_ID` diff --git a/.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh b/.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh index effd272..d13c046 100755 --- a/.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh +++ b/.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh @@ -103,7 +103,7 @@ detect_nearest_agent() { local base base="$(basename "$tok")" case "$base" in - claude|agy|hermes|grok) + claude|agy|hermes|grok|opencode) match="$base" break ;; @@ -127,6 +127,9 @@ detect_nearest_agent() { grok) detected_id=$(echo "$cmd_line" | grep -oE '(--session-id|--resume)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session-id|--resume)[[:space:]=]+//' || true) ;; + opencode) + detected_id=$(echo "$cmd_line" | grep -oE '(--session|-s)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session|-s)[[:space:]=]+//' || true) + ;; esac if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then @@ -141,6 +144,7 @@ detect_nearest_agent() { agy) env_var="${ANTIGRAVITY_CONVERSATION_ID:-}" ;; hermes) env_var="${HERMES_SESSION_ID:-}" ;; grok) env_var="${GROK_SESSION_ID:-}" ;; + opencode) env_var="${OPENCODE_SESSION_ID:-}" ;; esac if [ -n "$env_var" ] && is_valid_id "$env_var"; then diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index db13d28..e5db1cd 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -49,7 +49,7 @@ ideal resume path: `agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order: -1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free). +1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own` / `opencode_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free). 2. **Workspace-scoped on-disk scan** (adapter `discover()`) If both are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`. @@ -58,7 +58,7 @@ If both are empty → the workspace has no conversation yet. Fall back to `multi ```bash WORKSPACE=/path/to/project -AGENT=claude # claude | agy | hermes | grok — pass it explicitly +AGENT=claude # claude | agy | hermes | grok | opencode — pass it explicitly SESSION_NAME=-creator- # same convention as multi-agent-mux-create # Resolve the isolated herdr server name & load common utils diff --git a/.agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh b/.agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh index d2dac0b..082486c 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트 # Usage: -# bash resolve_session_id.sh --workspace --agent +# bash resolve_session_id.sh --workspace --agent # 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0) # # P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid @@ -13,7 +13,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" usage() { cat < --agent [--session ] +Usage: $0 --workspace --agent [--session ] Outputs the resolved UUID on stdout (empty if not found). --session prefers that registry row's recorded id; falls back to workspace-wide discovery if it does not verify. EOF @@ -36,8 +36,8 @@ done [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; } case "$AGENT" in - claude|agy|hermes|grok) ;; - *) echo "ERROR: --agent must be claude, agy, hermes, or grok" >&2; exit 2 ;; + claude|agy|hermes|grok|opencode) ;; + *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode" >&2; exit 2 ;; esac find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME" diff --git a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh index 3c42192..c99c476 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh @@ -9,7 +9,7 @@ source "$LIB_SH" usage() { cat < --agent --session [options] +Usage: $0 --workspace --agent --session [options] Options: --herdr-session NAME specify isolated herdr session name (alias: --herdr-server) @@ -42,7 +42,7 @@ done [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; } case "$AGENT" in - claude|agy|hermes|grok) ;; + claude|agy|hermes|grok|opencode) ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; esac [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; } @@ -103,6 +103,7 @@ if [ -z "$CMD_FULL" ]; then agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;; hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;; grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;; + opencode) CMD_FULL="${RESOLVED_BIN} --session $UUID --auto --agent build" ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; esac fi @@ -125,6 +126,12 @@ if [ "${DRY_RUN:-0}" = "1" ]; then exit 0 fi +if [ "$AGENT" = "opencode" ]; then + if [ -z "${OPENCODE_PERMISSION:-}" ]; then + export OPENCODE_PERMISSION='{"*":"allow"}' + fi +fi + # 4. Spawn new agent session (delegates to herdr translation shim) _herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CMD_FULL" if [ "$AGENT" = "claude" ]; then diff --git a/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh b/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh index 33bd5d8..e5833fd 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh @@ -4,14 +4,14 @@ # resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own) # 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e). # -# Usage: bash update_yaml_resumed.sh --session --uuid [--agent claude|agy|hermes|grok] +# Usage: bash update_yaml_resumed.sh --session --uuid [--agent claude|agy|hermes|grok|opencode] set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" usage() { cat < --uuid [--agent claude|agy|hermes|grok] [--herdr-session ] +Usage: $0 --session --uuid [--agent claude|agy|hermes|grok|opencode] [--herdr-session ] EOF } @@ -190,6 +190,20 @@ elif agent == 'hermes': cp = os.environ.get('CHILD_PID', '0') if cp.isdigit() and int(cp) > 0: target['child_pid'] = int(cp) +elif agent == 'grok': + target['pane']['cmd'] = 'grok' + target['pane']['cmd_full'] = f'grok --resume {uuid}' + target['grok_session_id_own'] = uuid + cp = os.environ.get('CHILD_PID', '0') + if cp.isdigit() and int(cp) > 0: + target['child_pid'] = int(cp) +elif agent == 'opencode': + target['pane']['cmd'] = 'opencode' + target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build' + target['opencode_session_id_own'] = uuid + cp = os.environ.get('CHILD_PID', '0') + if cp.isdigit() and int(cp) > 0: + target['child_pid'] = int(cp) snap = d.setdefault('snapshot', {}) snap['taken_at'] = now diff --git a/.agents/skills/multi-agent-mux-status/scripts/status.sh b/.agents/skills/multi-agent-mux-status/scripts/status.sh index af9c9dd..ae42f57 100755 --- a/.agents/skills/multi-agent-mux-status/scripts/status.sh +++ b/.agents/skills/multi-agent-mux-status/scripts/status.sh @@ -53,12 +53,12 @@ def resume_on_disk(s): name = s.get('name', '') cwd = (s.get('pane') or {}).get('cwd', '') agent = None - for a in ('claude', 'agy', 'hermes', 'grok'): + for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'): if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'): agent = a break if not agent: - for a in ('claude', 'agy', 'hermes', 'grok'): + for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'): if f"-{a}" in name or f"_{a}" in name: agent = a break @@ -97,6 +97,34 @@ def resume_on_disk(s): ws_slug = urllib.parse.quote(cwd, safe='') return 'yes' if os.path.exists(f"{home}/.grok/sessions/{ws_slug}/{u}/chat_history.jsonl") else 'MISSING' return 'no' + if agent == 'opencode': + u = s.get('opencode_session_id_own') + if u: + base_dir = f"{home}/.local/share/opencode" + odb = None + for cand in [f"{base_dir}/opencode.db", f"{base_dir}/storage.db"]: + if os.path.exists(cand): + odb = cand + break + if not odb and os.path.isdir(f"{base_dir}/project"): + for root, _, files in os.walk(f"{base_dir}/project"): + for f in files: + if f.endswith(".db"): + odb = os.path.join(root, f) + break + if odb: + break + if not odb or not os.path.exists(odb): + return 'MISSING' + try: + import sqlite3 + conn = sqlite3.connect(odb) + r = conn.execute("SELECT 1 FROM session WHERE id=?", (u,)).fetchone() + conn.close() + return 'yes' if r else 'MISSING' + except Exception: + return 'MISSING' + return 'no' return '?' @@ -202,18 +230,79 @@ def resume_on_disk(s): # workspace-SCOPED check only — per-row own id, never a global identity (P0-C) name = s.get('name', '') cwd = (s.get('pane') or {}).get('cwd', '') - if name.endswith('-creator-claude'): + agent = None + for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'): + if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'): + agent = a + break + if not agent: + for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'): + if f"-{a}" in name or f"_{a}" in name: + agent = a + break + + if agent == 'claude': u = s.get('claude_session_id_own') if u: key = cwd.replace('/', '-').replace('_', '-') return 'yes' if os.path.exists(f"{claude_project_dir}/{key}/{u}.jsonl") else 'MISSING' key = cwd.replace('/', '-').replace('_', '-') return 'scan' if glob.glob(f"{claude_project_dir}/{key}/*.jsonl") else 'no' - if name.endswith('-creator-agy'): + if agent == 'agy': u = s.get('agy_conversation_id_own') if u: return 'yes' if os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{u}.db") else 'MISSING' return 'no' + if agent == 'hermes': + u = s.get('hermes_conversation_id_own') + if u: + hdb = f"{home}/.hermes/state.db" + if not os.path.exists(hdb): + return 'MISSING' + try: + import sqlite3 + conn = sqlite3.connect(hdb) + r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (u,)).fetchone() + conn.close() + return 'yes' if r else 'MISSING' + except Exception: + return 'MISSING' + return 'no' + if agent == 'grok': + u = s.get('grok_session_id_own') + if u: + import urllib.parse + ws_slug = urllib.parse.quote(cwd, safe='') + return 'yes' if os.path.exists(f"{home}/.grok/sessions/{ws_slug}/{u}/chat_history.jsonl") else 'MISSING' + return 'no' + if agent == 'opencode': + u = s.get('opencode_session_id_own') + if u: + base_dir = f"{home}/.local/share/opencode" + odb = None + for cand in [f"{base_dir}/opencode.db", f"{base_dir}/storage.db"]: + if os.path.exists(cand): + odb = cand + break + if not odb and os.path.isdir(f"{base_dir}/project"): + for root, _, files in os.walk(f"{base_dir}/project"): + for f in files: + if f.endswith(".db"): + odb = os.path.join(root, f) + break + if odb: + break + if not odb or not os.path.exists(odb): + return 'MISSING' + try: + import sqlite3 + conn = sqlite3.connect(odb) + r = conn.execute("SELECT 1 FROM session WHERE id=?", (u,)).fetchone() + conn.close() + return 'yes' if r else 'MISSING' + except Exception: + return 'MISSING' + return 'no' return '?' diff --git a/.agents/skills/multi-agent-mux-stop/SKILL.md b/.agents/skills/multi-agent-mux-stop/SKILL.md index cdfefb5..b8b9389 100644 --- a/.agents/skills/multi-agent-mux-stop/SKILL.md +++ b/.agents/skills/multi-agent-mux-stop/SKILL.md @@ -37,7 +37,7 @@ The stop command is always **graceful by default**: ```bash SESSION_NAME=-creator- # convention -AGENT=claude # claude | agy | hermes | grok — always pass it +AGENT=claude # claude | agy | hermes | grok | opencode — always pass it AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml # 1) Session is registered? diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index bef3bf1..0f4def7 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # stop_session.sh — multi-agent-mux-stop 의 부속 스크립트 # Usage: -# bash stop_session.sh --session [--agent claude|agy|hermes|grok] [--herdr-session ] \ +# bash stop_session.sh --session [--agent claude|agy|hermes|grok|opencode] [--herdr-session ] \ # [--reason ] [--purge-conversation] [--yes] # # 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고 @@ -12,7 +12,7 @@ # # 옵션: # --session — 대상 세션 (필수) -# --agent — claude | agy | hermes | grok +# --agent — claude | agy | hermes | grok | opencode # (권장: 항상 명시. 미지정 시 레지스트리 기록으로 # 해석 — agent 필드 → 세션명 접미사 → pane.cmd; # 셋 다 실패하면 exit 2) @@ -91,8 +91,8 @@ while [ $# -gt 0 ]; do done if [ -n "$AGENT" ]; then case "$AGENT" in - claude|agy|hermes|grok) ;; - *) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok." >&2; exit 2 ;; + claude|agy|hermes|grok|opencode) ;; + *) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode." >&2; exit 2 ;; esac fi [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; } @@ -287,6 +287,8 @@ if captured and not purge: target['hermes_conversation_id_own'] = captured elif agent == 'grok': target['grok_session_id_own'] = captured + elif agent == 'opencode': + target['opencode_session_id_own'] = captured target['resumable'] = True if purge and purge_uuid: