Fix auto-kickoff: short message + raw TTY relay

Long kickoff text tripped Hermes paste-collapse ([Pasted text #N]) and
broke Enter handling. Use a short single-line kickoff, inject once after
skill activation, and set raw mode so input stays inside Hermes.
This commit is contained in:
domfelipe 2026-08-03 00:36:38 -03:00
parent ec5a72865e
commit b8f55e799a
3 changed files with 50 additions and 55 deletions

View file

@ -7,7 +7,7 @@ set -euo pipefail
SKILL_NAME="hermes-client-onboarding" SKILL_NAME="hermes-client-onboarding"
DEFAULT_BASE="${HERMES_ONBOARD_BASE:-https://setup.domhubs.com.br/hermes}" DEFAULT_BASE="${HERMES_ONBOARD_BASE:-https://setup.domhubs.com.br/hermes}"
HERMES_INSTALL_URL="${HERMES_INSTALL_URL:-https://hermes-agent.nousresearch.com/install.sh}" HERMES_INSTALL_URL="${HERMES_INSTALL_URL:-https://hermes-agent.nousresearch.com/install.sh}"
KICKOFF_MSG="${HERMES_ONBOARD_KICKOFF:-Inicie AGORA o onboarding de cliente Hermes. Siga a skill hermes-client-onboarding: pre-flight em silêncio e abra a Phase 1 com a primeira pergunta. Você fala primeiro — não espere eu dizer oi. Português brasileiro.}" KICKOFF_MSG="${HERMES_ONBOARD_KICKOFF:-Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro).}"
CONDUCTOR="${HERMES_ONBOARD_CONDUCTOR:-}" CONDUCTOR="${HERMES_ONBOARD_CONDUCTOR:-}"
NO_LAUNCH=0 NO_LAUNCH=0

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Spawn `hermes chat --cli -s <skill>` and inject the kickoff as first user message. """Spawn hermes chat --cli -s <skill> and submit a short kickoff once.
Hermes only runs a model turn after a user message. The TUI startup-query path Avoids Hermes paste-collapse (5 lines or 2000 chars [Pasted text #N]).
races session creation (~4s) and often silently skips. This classic-CLI path Uses raw TTY relay so Enter stays inside Hermes, not the outer shell.
waits for a ready prompt, sends the kickoff once, then hands the TTY to the user.
""" """
from __future__ import annotations from __future__ import annotations
@ -11,40 +10,49 @@ import os
import pty import pty
import select import select
import sys import sys
import termios
import time 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)."
)
def main() -> int: def main() -> int:
skill = os.environ.get("HERMES_ONBOARD_SKILL", "hermes-client-onboarding") skill = os.environ.get("HERMES_ONBOARD_SKILL", "hermes-client-onboarding")
kickoff = os.environ.get( kickoff = os.environ.get("HERMES_ONBOARD_KICKOFF", DEFAULT_KICKOFF).strip()
"HERMES_ONBOARD_KICKOFF", # Collapse accidental newlines so we never trip paste_collapse by lines
"Inicie AGORA o onboarding de cliente Hermes. Siga a skill " kickoff = " ".join(kickoff.split())
"hermes-client-onboarding: pre-flight em silêncio e abra a Phase 1 " if len(kickoff) > 400:
"com a primeira pergunta. Você fala primeiro. Português brasileiro.", kickoff = kickoff[:397] + "..."
)
if not kickoff.endswith("\n"):
kickoff += "\n"
argv = ["hermes", "chat", "--cli", "-s", skill] argv = ["hermes", "chat", "--cli", "-s", skill]
# Extra args after --
if len(sys.argv) > 1: if len(sys.argv) > 1:
argv.extend(sys.argv[1:]) argv.extend(sys.argv[1:])
pid, master = pty.fork() pid, master = pty.fork()
if pid == 0: if pid == 0:
os.environ.pop("HERMES_TUI_QUERY", None) # avoid confusing classic CLI
os.execvp(argv[0], argv) os.execvp(argv[0], argv)
# Parent: relay I/O; inject kickoff once after session looks ready. stdin_fd = sys.stdin.fileno()
stdout_fd = sys.stdout.fileno()
old_tty = None
if sys.stdin.isatty():
old_tty = termios.tcgetattr(stdin_fd)
tty.setraw(stdin_fd)
sent = False sent = False
buf = b"" buf = b""
start = time.time() start = time.time()
inject_after = 1.5 # min wait for banner # Wait for skill activation line, else inject after a few seconds
deadline = start + 45.0
try: try:
while True: while True:
timeout = 0.2 r, _, _ = select.select([master, stdin_fd], [], [], 0.15)
r, _, _ = select.select([master, sys.stdin], [], [], timeout)
now = time.time() now = time.time()
if master in r: if master in r:
@ -54,49 +62,30 @@ def main() -> int:
data = b"" data = b""
if not data: if not data:
break break
os.write(sys.stdout.fileno(), data) os.write(stdout_fd, data)
buf += data buf += data
if len(buf) > 20000: if len(buf) > 30000:
buf = buf[-10000:] buf = buf[-12000:]
if sys.stdin in r: if stdin_fd in r:
try: try:
data = os.read(sys.stdin.fileno(), 8192) data = os.read(stdin_fd, 8192)
except OSError: except OSError:
data = b"" data = b""
if not data: if data:
# stdin closed — keep agent until it exits
pass
else:
os.write(master, data) os.write(master, data)
if not sent and now >= start + inject_after: if not sent:
lower = buf.lower() lower = buf.lower()
ready_markers = ( activated = b"activated skills" in lower or b"hermes-client-onboarding" in lower
b"ready", timed = now >= start + 3.5
b"session:", if (activated and now >= start + 1.2) or timed:
b"try ", time.sleep(0.25)
b"welcome", # Type as normal keys + CR (not a giant paste burst)
b"type your message", payload = (kickoff + "\r").encode("utf-8", errors="replace")
b"\n> ", os.write(master, payload)
"\n".encode("utf-8"),
b"\nprompt",
b"hermes",
)
looks_ready = any(m in lower for m in ready_markers)
timed = now >= start + 4.0 # hard fallback inject
if looks_ready or timed:
# Small settle so status line finishes drawing
time.sleep(0.35)
os.write(master, kickoff.encode("utf-8", errors="replace"))
sent = True sent = True
if not sent and now > deadline:
# Last resort
os.write(master, kickoff.encode("utf-8", errors="replace"))
sent = True
# Reap child
wpid, status = os.waitpid(pid, os.WNOHANG) wpid, status = os.waitpid(pid, os.WNOHANG)
if wpid == pid: if wpid == pid:
if os.WIFEXITED(status): if os.WIFEXITED(status):
@ -109,12 +98,16 @@ def main() -> int:
pass pass
return 130 return 130
finally: finally:
if old_tty is not None:
try:
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
except termios.error:
pass
try: try:
os.close(master) os.close(master)
except OSError: except OSError:
pass pass
# Blocking wait if loop broke on EOF from master
try: try:
_, status = os.waitpid(pid, 0) _, status = os.waitpid(pid, 0)
if os.WIFEXITED(status): if os.WIFEXITED(status):

View file

@ -8,7 +8,9 @@ set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}" export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}"
SKILL_NAME="${HERMES_ONBOARD_SKILL:-hermes-client-onboarding}" SKILL_NAME="${HERMES_ONBOARD_SKILL:-hermes-client-onboarding}"
KICKOFF="${HERMES_ONBOARD_KICKOFF:-Inicie AGORA o onboarding de cliente Hermes. Siga a skill hermes-client-onboarding: execute o pre-flight em silêncio e abra a Phase 1 fazendo a primeira pergunta ao usuário. Você fala primeiro — não espere eu dizer oi ou começar. Português brasileiro.}" # Short on purpose: long kickoffs hit Hermes paste-collapse (≥5 lines / 2000 chars)
# and leave a stuck [Pasted text #N] instead of submitting.
KICKOFF="${HERMES_ONBOARD_KICKOFF:-Inicie o onboarding agora. Skill hermes-client-onboarding. Pre-flight silencioso e Phase 1 (voce fala primeiro).}"
export HERMES_ONBOARD_SKILL="$SKILL_NAME" export HERMES_ONBOARD_SKILL="$SKILL_NAME"
export HERMES_ONBOARD_KICKOFF="$KICKOFF" export HERMES_ONBOARD_KICKOFF="$KICKOFF"