mirror of
https://github.com/domfelipe/hermes-client-onboarding.git
synced 2026-08-07 05:16:42 +00:00
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:
parent
ec5a72865e
commit
b8f55e799a
3 changed files with 50 additions and 55 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue