feat(opencode): wire 29-point CLI lifecycle scripts, drift-C reconciler, and skill docs
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
||||
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
||||
#
|
||||
# 동작:
|
||||
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
|
||||
@@ -26,11 +26,11 @@ source "$_lib_sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> --role <role> [options]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <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/<session> 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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <uuid>` / `claude --session-id <uuid>`
|
||||
- `agy --conversation <uuid>`
|
||||
- `grok --session-id <uuid>` / `grok --resume <uuid>`
|
||||
- `opencode --session <uuid>`
|
||||
2. **Family-Matched Environment Variables**:
|
||||
- `claude` → `CLAUDE_CODE_SESSION_ID`
|
||||
- `agy` → `ANTIGRAVITY_CONVERSATION_ID`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
|
||||
|
||||
# Resolve the isolated herdr server name & load common utils
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok>
|
||||
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode>
|
||||
# 출력: 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 <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> [--session <name>]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> [--session <name>]
|
||||
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"
|
||||
|
||||
@@ -9,7 +9,7 @@ source "$LIB_SH"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> --session <name> [options]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --session <name> [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
|
||||
|
||||
@@ -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 <name> --uuid <id> [--agent claude|agy|hermes|grok]
|
||||
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode]
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok] [--herdr-session <name>]
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>]
|
||||
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
|
||||
|
||||
@@ -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 '?'
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ The stop command is always **graceful by default**:
|
||||
|
||||
```bash
|
||||
SESSION_NAME=<workspace>-creator-<agent> # 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?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok] [--herdr-session <name>] \
|
||||
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] \
|
||||
# [--reason <reason>] [--purge-conversation] [--yes]
|
||||
#
|
||||
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# 옵션:
|
||||
# --session <name> — 대상 세션 (필수)
|
||||
# --agent <type> — claude | agy | hermes | grok
|
||||
# --agent <type> — 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:
|
||||
|
||||
Reference in New Issue
Block a user