From 1ec89304269815bf3325f61df280c42804830b66 Mon Sep 17 00:00:00 2001 From: domfelipe Date: Mon, 3 Aug 2026 00:48:01 -0300 Subject: [PATCH] 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. --- README.md | 11 +++- .../scripts/auto_kickoff_cli.py | 64 +++++++++++++------ .../scripts/start-onboarding.sh | 53 ++++++++++----- 3 files changed, 91 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 8c0e0a0..40af4d1 100644 --- a/README.md +++ b/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- + +# TUI Ink (opcional, kickoff frágil) HERMES_ONBOARD_USE_TUI=1 hermes-client-onboarding ``` diff --git a/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py b/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py index 346b74b..9f78453 100755 --- a/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py +++ b/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py @@ -1,31 +1,43 @@ #!/usr/bin/env python3 -"""Spawn hermes chat --cli -s 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 diff --git a/skill/hermes-client-onboarding/scripts/start-onboarding.sh b/skill/hermes-client-onboarding/scripts/start-onboarding.sh index 4095a4b..93acb7d 100755 --- a/skill/hermes-client-onboarding/scripts/start-onboarding.sh +++ b/skill/hermes-client-onboarding/scripts/start-onboarding.sh @@ -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/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 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" "$@"