mirror of
https://github.com/domfelipe/hermes-client-onboarding.git
synced 2026-08-07 04:16:43 +00:00
Use tmux for stable Hermes auto-kickoff (avoid PTY freeze)
PTY relay freezes prompt_toolkit after the first turn. Prefer a detached tmux session, send short kickoff via send-keys, then attach. Keep PTY as fallback when tmux is missing.
This commit is contained in:
parent
b8f55e799a
commit
1ec8930426
3 changed files with 91 additions and 37 deletions
11
README.md
11
README.md
|
|
@ -155,10 +155,17 @@ O bootstrap instala `~/.local/bin/hermes-client-onboarding`:
|
|||
hermes-client-onboarding
|
||||
```
|
||||
|
||||
Por padrão usa **CLI clássico + injeção automática** da primeira mensagem (PTY). O TUI nativo do Hermes tem race no startup-query (~4s) e costuma ficar mudo — por isso não é o default.
|
||||
Por padrão usa **tmux** (Hermes em sessão dedicada + `send-keys` do kickoff). Isso evita o freeze do wrapper PTY com prompt_toolkit.
|
||||
|
||||
```bash
|
||||
# TUI (opcional, menos confiável para auto-start)
|
||||
# se pedir tmux e não tiver:
|
||||
# apt install -y tmux
|
||||
|
||||
hermes-client-onboarding
|
||||
# detach: Ctrl-b d
|
||||
# reattach: tmux ls && tmux attach -t hermes-onboard-<pid>
|
||||
|
||||
# TUI Ink (opcional, kickoff frágil)
|
||||
HERMES_ONBOARD_USE_TUI=1 hermes-client-onboarding
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Spawn hermes chat --cli -s <skill> and submit a short kickoff once.
|
||||
"""Fallback: spawn hermes chat --cli and inject a short kickoff via PTY.
|
||||
|
||||
Avoids Hermes paste-collapse (≥5 lines or ≥2000 chars → [Pasted text #N]).
|
||||
Uses raw TTY relay so Enter stays inside Hermes, not the outer shell.
|
||||
Prefer tmux path in start-onboarding.sh — this can freeze prompt_toolkit
|
||||
on some terminals. Kept for hosts without tmux.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
|
||||
|
||||
# Keep under paste_collapse thresholds (5 lines / 2000 chars)
|
||||
DEFAULT_KICKOFF = (
|
||||
"Inicie o onboarding agora. Skill hermes-client-onboarding. "
|
||||
"Pre-flight silencioso e abra a Phase 1 (voce fala primeiro)."
|
||||
"Pre-flight silencioso e Phase 1 (voce fala primeiro)."
|
||||
)
|
||||
|
||||
|
||||
def _set_winsize(fd: int) -> None:
|
||||
try:
|
||||
import shutil
|
||||
|
||||
cols, rows = shutil.get_terminal_size(fallback=(120, 40))
|
||||
packed = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
skill = os.environ.get("HERMES_ONBOARD_SKILL", "hermes-client-onboarding")
|
||||
kickoff = os.environ.get("HERMES_ONBOARD_KICKOFF", DEFAULT_KICKOFF).strip()
|
||||
# Collapse accidental newlines so we never trip paste_collapse by lines
|
||||
kickoff = " ".join(kickoff.split())
|
||||
if len(kickoff) > 400:
|
||||
kickoff = kickoff[:397] + "..."
|
||||
|
|
@ -36,9 +48,23 @@ def main() -> int:
|
|||
|
||||
pid, master = pty.fork()
|
||||
if pid == 0:
|
||||
os.environ.pop("HERMES_TUI_QUERY", None) # avoid confusing classic CLI
|
||||
os.environ.pop("HERMES_TUI_QUERY", None)
|
||||
os.execvp(argv[0], argv)
|
||||
|
||||
_set_winsize(master)
|
||||
|
||||
def _on_winch(_sig: int, _frame: object) -> None:
|
||||
_set_winsize(master)
|
||||
try:
|
||||
os.kill(pid, signal.SIGWINCH)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, _on_winch)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
stdin_fd = sys.stdin.fileno()
|
||||
stdout_fd = sys.stdout.fileno()
|
||||
old_tty = None
|
||||
|
|
@ -49,10 +75,9 @@ def main() -> int:
|
|||
sent = False
|
||||
buf = b""
|
||||
start = time.time()
|
||||
# Wait for skill activation line, else inject after a few seconds
|
||||
try:
|
||||
while True:
|
||||
r, _, _ = select.select([master, stdin_fd], [], [], 0.15)
|
||||
r, _, _ = select.select([master, stdin_fd], [], [], 0.12)
|
||||
now = time.time()
|
||||
|
||||
if master in r:
|
||||
|
|
@ -77,23 +102,22 @@ def main() -> int:
|
|||
|
||||
if not sent:
|
||||
lower = buf.lower()
|
||||
activated = b"activated skills" in lower or b"hermes-client-onboarding" in lower
|
||||
timed = now >= start + 3.5
|
||||
if (activated and now >= start + 1.2) or timed:
|
||||
time.sleep(0.25)
|
||||
# Type as normal keys + CR (not a giant paste burst)
|
||||
payload = (kickoff + "\r").encode("utf-8", errors="replace")
|
||||
os.write(master, payload)
|
||||
ready = (
|
||||
b"activated skills" in lower
|
||||
or b"welcome to hermes" in lower
|
||||
or b"type your message" in lower
|
||||
)
|
||||
if (ready and now >= start + 1.0) or now >= start + 4.0:
|
||||
time.sleep(0.35)
|
||||
os.write(master, (kickoff + "\r").encode("utf-8", errors="replace"))
|
||||
sent = True
|
||||
|
||||
wpid, status = os.waitpid(pid, os.WNOHANG)
|
||||
if wpid == pid:
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
return 1
|
||||
return os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1
|
||||
except KeyboardInterrupt:
|
||||
try:
|
||||
os.kill(pid, 2)
|
||||
os.kill(pid, signal.SIGINT)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
return 130
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
# Launch Hermes with hermes-client-onboarding; agent speaks first (Phase 1).
|
||||
#
|
||||
# Default: classic CLI + PTY auto-kickoff (reliable).
|
||||
# Optional: HERMES_ONBOARD_USE_TUI=1 for Ink TUI (env HERMES_TUI_QUERY; may race).
|
||||
# Preferred: tmux session + send-keys (stable full-screen Hermes, no PTY freeze).
|
||||
# Fallback: Python PTY inject (can be flaky with prompt_toolkit).
|
||||
# Optional: HERMES_ONBOARD_USE_TUI=1 for Ink TUI (startup-query race).
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
SKILL_NAME="${HERMES_ONBOARD_SKILL:-hermes-client-onboarding}"
|
||||
# Short on purpose: long kickoffs hit Hermes paste-collapse (≥5 lines / 2000 chars)
|
||||
# and leave a stuck [Pasted text #N] instead of submitting.
|
||||
# Short: long text trips Hermes paste-collapse
|
||||
KICKOFF="${HERMES_ONBOARD_KICKOFF:-Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro).}"
|
||||
KICKOFF="$(printf '%s' "$KICKOFF" | tr '\n' ' ' | sed 's/ */ /g')"
|
||||
|
||||
export HERMES_ONBOARD_SKILL="$SKILL_NAME"
|
||||
export HERMES_ONBOARD_KICKOFF="$KICKOFF"
|
||||
# TUI path (also set so HERMES_TUI=1 launches pick up kickoff)
|
||||
export HERMES_TUI_SKILLS="$SKILL_NAME"
|
||||
export HERMES_TUI_QUERY="$KICKOFF"
|
||||
|
||||
|
|
@ -24,37 +24,60 @@ if ! command -v hermes >/dev/null 2>&1; then
|
|||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# When installed as ~/.local/bin/hermes-client-onboarding, companion lives with skill
|
||||
AUTO_PY=""
|
||||
for candidate in \
|
||||
"${SCRIPT_DIR}/auto_kickoff_cli.py" \
|
||||
"${HOME}/.hermes/skills/${SKILL_NAME}/scripts/auto_kickoff_cli.py" \
|
||||
"${HOME}/.local/share/hermes-client-onboarding/auto_kickoff_cli.py"
|
||||
do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
AUTO_PY="$candidate"
|
||||
break
|
||||
fi
|
||||
[[ -f "$candidate" ]] && AUTO_PY="$candidate" && break
|
||||
done
|
||||
|
||||
use_tui="${HERMES_ONBOARD_USE_TUI:-0}"
|
||||
|
||||
if [[ "$use_tui" == "1" ]]; then
|
||||
# Explicit env + --query (TUI maps -q → HERMES_TUI_QUERY; keep both)
|
||||
if [[ -r /dev/tty ]]; then
|
||||
exec hermes chat --tui -s "$SKILL_NAME" --query "$KICKOFF" </dev/tty
|
||||
fi
|
||||
exec hermes chat --tui -s "$SKILL_NAME" --query "$KICKOFF"
|
||||
fi
|
||||
|
||||
# Reliable path: classic CLI + inject first user message
|
||||
# --- Preferred: tmux (no frozen PTY wrapper) ---
|
||||
if command -v tmux >/dev/null 2>&1 && [[ -t 0 && -t 1 ]]; then
|
||||
SESSION="hermes-onboard-$$"
|
||||
# Kill leftover same-name (shouldn't happen with $$)
|
||||
tmux has-session -t "$SESSION" 2>/dev/null && tmux kill-session -t "$SESSION" 2>/dev/null || true
|
||||
|
||||
tmux new-session -d -s "$SESSION" -x "$(tput cols 2>/dev/null || echo 120)" -y "$(tput lines 2>/dev/null || echo 40)" \
|
||||
"export PATH=\"${PATH}\"; hermes chat --cli -s ${SKILL_NAME}; exec bash"
|
||||
|
||||
# Wait until Hermes is up, then type kickoff + Enter
|
||||
for i in $(seq 1 40); do
|
||||
# capture pane; look for skill activation or welcome
|
||||
pane="$(tmux capture-pane -t "$SESSION" -p 2>/dev/null || true)"
|
||||
if printf '%s' "$pane" | grep -qiE 'Activated skills|Welcome to Hermes|hermes-client-onboarding'; then
|
||||
sleep 0.6
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
sleep 0.4
|
||||
# send-keys: literal string then Enter (C-m)
|
||||
tmux send-keys -t "$SESSION" -l -- "$KICKOFF"
|
||||
sleep 0.15
|
||||
tmux send-keys -t "$SESSION" C-m
|
||||
|
||||
echo "==> Sessão tmux: $SESSION (detach: Ctrl-b d | reattach: tmux attach -t $SESSION)"
|
||||
exec tmux attach -t "$SESSION"
|
||||
fi
|
||||
|
||||
# --- Fallback: PTY inject ---
|
||||
if [[ -n "$AUTO_PY" ]] && command -v python3 >/dev/null 2>&1; then
|
||||
echo "warn: tmux not found — using PTY fallback (se travar, instale: apt install -y tmux)" >&2
|
||||
if [[ -r /dev/tty ]]; then
|
||||
exec python3 "$AUTO_PY" "$@" </dev/tty >/dev/tty 2>/dev/tty
|
||||
fi
|
||||
exec python3 "$AUTO_PY" "$@"
|
||||
fi
|
||||
|
||||
# Last resort: one-shot (not interactive after)
|
||||
echo "warn: auto_kickoff_cli.py missing — running one-shot kickoff only" >&2
|
||||
echo "warn: no tmux/python auto-kickoff — one-shot only" >&2
|
||||
exec hermes chat -s "$SKILL_NAME" -Q -q "$KICKOFF" "$@"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue