feat: add support for hermes agent in tmux orchestration scripts
This commit is contained in:
@@ -438,6 +438,19 @@ def db_exists(uuid):
|
||||
return os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{uuid}.db")
|
||||
|
||||
|
||||
def hermes_exists(uuid):
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||
conn.close()
|
||||
return r is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def emit(u):
|
||||
print(u)
|
||||
raise SystemExit(0)
|
||||
@@ -483,6 +496,10 @@ for s in sessions:
|
||||
cand = s.get('agy_conversation_id_own')
|
||||
if cand and db_exists(cand):
|
||||
emit(cand)
|
||||
if agent == 'hermes' and name.endswith('-creator-hermes'):
|
||||
cand = s.get('hermes_conversation_id_own')
|
||||
if cand and hermes_exists(cand):
|
||||
emit(cand)
|
||||
|
||||
# 2) disk scan scoped to THIS workspace
|
||||
if agent == 'claude':
|
||||
@@ -511,6 +528,20 @@ elif agent == 'agy':
|
||||
cand = None
|
||||
if cand and db_exists(cand):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
cand = None
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ws,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
except Exception:
|
||||
cand = None
|
||||
if cand:
|
||||
emit(cand)
|
||||
|
||||
# 3) agent_identities cache, ONLY when its project_cwd == this workspace
|
||||
ai = {}
|
||||
@@ -538,6 +569,10 @@ if ai_agent.get('project_cwd') == ws:
|
||||
cand = ai.get('conversation_id')
|
||||
if cand and db_exists(cand):
|
||||
emit(cand)
|
||||
elif agent == 'hermes':
|
||||
cand = ai_agent.get('session_id') or ai.get('conversation_id')
|
||||
if cand and hermes_exists(cand):
|
||||
emit(cand)
|
||||
|
||||
print('')
|
||||
PYEOF
|
||||
|
||||
@@ -23,11 +23,11 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy> [options]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes> [options]
|
||||
|
||||
Options:
|
||||
--workspace PATH project directory (required)
|
||||
--agent AGENT claude | agy (required)
|
||||
--agent AGENT claude | agy | hermes (required)
|
||||
--session NAME tmux session name (default: derived from workspace)
|
||||
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
|
||||
--dry-run print commands without executing
|
||||
@@ -70,7 +70,7 @@ fi
|
||||
command -v tmux >/dev/null || { echo "ERROR: tmux not installed" >&2; exit 1; }
|
||||
command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; }
|
||||
|
||||
# Auth Check (OAuth check for agy, loggedIn check for claude)
|
||||
# Auth Check (OAuth check for agy, loggedIn check for claude, status for hermes)
|
||||
if [ "$AGENT" = "claude" ]; then
|
||||
if ! claude auth status 2>/dev/null | grep -q '"loggedIn":\s*true'; then
|
||||
echo "ERROR: claude not logged in. Run 'claude auth login' first." >&2
|
||||
@@ -81,6 +81,11 @@ elif [ "$AGENT" = "agy" ]; then
|
||||
echo "ERROR: agy is not authenticated. Please log in first." >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$AGENT" = "hermes" ]; then
|
||||
if ! hermes status >/dev/null 2>&1; then
|
||||
echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
|
||||
@@ -111,7 +116,10 @@ spawn() {
|
||||
agy)
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
||||
;;
|
||||
*) echo "ERROR: --agent must be claude or agy, got: $AGENT" >&2; exit 2 ;;
|
||||
hermes)
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes"
|
||||
;;
|
||||
*) echo "ERROR: --agent must be claude, agy or hermes, got: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -136,6 +144,7 @@ NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL='claude' ;;
|
||||
agy) CMD_FULL='agy --dangerously-skip-permissions' ;;
|
||||
hermes) CMD_FULL='hermes' ;;
|
||||
esac
|
||||
|
||||
# 시작 명령
|
||||
@@ -152,7 +161,7 @@ case "$AGENT" in
|
||||
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude\""
|
||||
fi
|
||||
;;
|
||||
agy)
|
||||
agy|hermes)
|
||||
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
|
||||
;;
|
||||
esac
|
||||
@@ -163,6 +172,8 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then
|
||||
delegate_agent=""
|
||||
if [ "$AGENT" = "claude" ]; then
|
||||
delegate_agent="claude-code"
|
||||
elif [ "$AGENT" = "hermes" ]; then
|
||||
delegate_agent="hermes-agent"
|
||||
else
|
||||
delegate_agent="antigravity-cli"
|
||||
fi
|
||||
@@ -180,8 +191,8 @@ fi
|
||||
# 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B).
|
||||
# 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터).
|
||||
CHILD_PID=0
|
||||
if [ "$AGENT" = "agy" ] && [ -n "$PANE_PID" ]; then
|
||||
CHILD_PID=$(pgrep -P "$PANE_PID" -x agy 2>/dev/null | head -1 || true)
|
||||
if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then
|
||||
CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true)
|
||||
CHILD_PID="${CHILD_PID:-0}"
|
||||
fi
|
||||
|
||||
@@ -249,6 +260,11 @@ elif agent == 'agy':
|
||||
}
|
||||
]
|
||||
entry['last_visible_status'] = "TUI started; awaiting first user message"
|
||||
elif agent == 'hermes':
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
||||
entry['hermes_conversation_id_own'] = None
|
||||
entry['last_visible_status'] = "TUI started; awaiting first user message"
|
||||
|
||||
sessions.append(entry)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ If all three are empty → the workspace has no conversation yet. Fall back to `
|
||||
|
||||
```bash
|
||||
WORKSPACE=/path/to/project
|
||||
AGENT=claude # or agy
|
||||
AGENT=claude # or agy or hermes
|
||||
SESSION_NAME=<workspace>-creator-<agent> # same convention as tmux-agent-orchestrate-create
|
||||
|
||||
# 1. Resolve the session id
|
||||
@@ -100,6 +100,10 @@ case "$AGENT" in
|
||||
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" \
|
||||
"agy --dangerously-skip-permissions --conversation $UUID"
|
||||
;;
|
||||
hermes)
|
||||
tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" \
|
||||
"hermes --resume $UUID"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 4. Update agent-sessions.yaml: status running, last_visible_status
|
||||
|
||||
@@ -33,8 +33,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) ;;
|
||||
*) echo "ERROR: --agent must be claude or agy" >&2; exit 2 ;;
|
||||
claude|agy|hermes) ;;
|
||||
*) echo "ERROR: --agent must be claude or agy or hermes" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
find_workspace_uuid "$WORKSPACE" "$AGENT"
|
||||
|
||||
@@ -40,6 +40,7 @@ if [ -z "$AGENT" ]; then
|
||||
case "$SESSION_NAME" in
|
||||
*-creator-claude) AGENT=claude ;;
|
||||
*-creator-agy) AGENT=agy ;;
|
||||
*-creator-hermes) AGENT=hermes ;;
|
||||
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
@@ -50,8 +51,8 @@ NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
PANE_PID=$(tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
|
||||
PANE_PID="${PANE_PID:-}"
|
||||
CHILD_PID=0
|
||||
if [ "$AGENT" = "agy" ] && [ -n "$PANE_PID" ]; then
|
||||
CHILD_PID=$(pgrep -P "$PANE_PID" -x agy 2>/dev/null | head -1 || true)
|
||||
if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then
|
||||
CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true)
|
||||
CHILD_PID="${CHILD_PID:-0}"
|
||||
fi
|
||||
|
||||
@@ -136,6 +137,13 @@ elif agent == 'agy':
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
elif agent == 'hermes':
|
||||
target['pane']['cmd'] = 'hermes'
|
||||
target['pane']['cmd_full'] = f'hermes --resume {uuid}'
|
||||
target['hermes_conversation_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
|
||||
|
||||
@@ -75,6 +75,7 @@ if [ -z "$AGENT" ]; then
|
||||
case "$SESSION_NAME" in
|
||||
*-creator-claude) AGENT=claude ;;
|
||||
*-creator-agy) AGENT=agy ;;
|
||||
*-creator-hermes) AGENT=hermes ;;
|
||||
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
@@ -182,6 +183,7 @@ graceful_stop() {
|
||||
case "$AGENT" in
|
||||
claude) exitkey="/exit" ;;
|
||||
agy) exitkey="Exit" ;;
|
||||
hermes) exitkey="/exit" ;;
|
||||
*) exitkey="/exit" ;;
|
||||
esac
|
||||
echo "graceful: send-keys '$exitkey' to $SESSION_NAME"
|
||||
@@ -259,6 +261,8 @@ if captured and not purge:
|
||||
target['claude_session_id_own'] = captured
|
||||
elif agent == 'agy':
|
||||
target['agy_conversation_id_own'] = captured
|
||||
elif agent == 'hermes':
|
||||
target['hermes_conversation_id_own'] = captured
|
||||
target['resumable'] = True
|
||||
|
||||
# --purge-conversation: 워크스페이스 격리된 UUID 의 디스크 artifact 만 삭제 (P0-C)
|
||||
@@ -281,6 +285,24 @@ if purge and purge_uuid:
|
||||
shutil.rmtree(brain)
|
||||
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
|
||||
conn = sqlite3.connect(hdb)
|
||||
conn.execute("DELETE FROM sessions WHERE id=?", (purge_uuid,))
|
||||
conn.execute("DELETE FROM messages WHERE session_id=?", (purge_uuid,))
|
||||
conn.commit()
|
||||
conn.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
|
||||
# agent_identities 는 cache — 이 워크스페이스 것일 때만 비운다
|
||||
ai = (d.get('agent_identities') or {}).get(agent) or {}
|
||||
if ai.get('project_cwd') == ws:
|
||||
@@ -293,6 +315,8 @@ if purge and 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 purge and not purge_uuid:
|
||||
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user