mirror of
https://github.com/domfelipe/hermes-client-onboarding.git
synced 2026-08-07 11:56:43 +00:00
Reliable auto-kickoff via classic CLI PTY inject
TUI HERMES_TUI_QUERY races session creation (~4s) and often never submits. Default launcher now uses hermes chat --cli plus a small Python PTY that injects the Phase-1 kickoff after ready markers.
This commit is contained in:
parent
29c3fa703b
commit
d8bea2017b
5 changed files with 204 additions and 23 deletions
10
README.md
10
README.md
|
|
@ -149,14 +149,18 @@ Não imprime secrets.
|
|||
|
||||
## Launcher (agente fala primeiro)
|
||||
|
||||
O bootstrap instala `~/.local/bin/hermes-client-onboarding`, que abre o TUI com kickoff automático:
|
||||
O bootstrap instala `~/.local/bin/hermes-client-onboarding`:
|
||||
|
||||
```bash
|
||||
hermes-client-onboarding
|
||||
```
|
||||
|
||||
Isso usa `hermes chat --tui -s hermes-client-onboarding -q "…"` — o Hermes **envia a primeira mensagem sozinho** e continua interativo.
|
||||
`hermes chat -s …` sem `-q` / sem launcher espera o usuário falar primeiro.
|
||||
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.
|
||||
|
||||
```bash
|
||||
# TUI (opcional, menos confiável para auto-start)
|
||||
HERMES_ONBOARD_USE_TUI=1 hermes-client-onboarding
|
||||
```
|
||||
|
||||
## Licença
|
||||
|
||||
|
|
|
|||
31
install.sh
31
install.sh
|
|
@ -107,6 +107,9 @@ copy_tree() {
|
|||
if [[ -f "$dest/scripts/start-onboarding.sh" ]]; then
|
||||
chmod +x "$dest/scripts/start-onboarding.sh"
|
||||
fi
|
||||
if [[ -f "$dest/scripts/auto_kickoff_cli.py" ]]; then
|
||||
chmod +x "$dest/scripts/auto_kickoff_cli.py"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_skill_to() {
|
||||
|
|
@ -119,8 +122,10 @@ fetch_skill_to() {
|
|||
curl -fsSL "${base}/skill/${SKILL_NAME}/references/troubleshooting.md" -o "$dest/references/troubleshooting.md"
|
||||
curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/apply-core-config.sh" -o "$dest/scripts/apply-core-config.sh"
|
||||
curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/start-onboarding.sh" -o "$dest/scripts/start-onboarding.sh" || true
|
||||
curl -fsSL "${base}/skill/${SKILL_NAME}/scripts/auto_kickoff_cli.py" -o "$dest/scripts/auto_kickoff_cli.py" || true
|
||||
chmod +x "$dest/scripts/apply-core-config.sh"
|
||||
[[ -f "$dest/scripts/start-onboarding.sh" ]] && chmod +x "$dest/scripts/start-onboarding.sh"
|
||||
[[ -f "$dest/scripts/auto_kickoff_cli.py" ]] && chmod +x "$dest/scripts/auto_kickoff_cli.py"
|
||||
[[ -s "$dest/SKILL.md" ]] || die "failed to download SKILL.md from $base"
|
||||
}
|
||||
|
||||
|
|
@ -142,13 +147,18 @@ install_skill() {
|
|||
copy_tree "$staging" "$hermes_dest"
|
||||
log "Skill installed for Hermes → $hermes_dest"
|
||||
|
||||
# Launcher: auto-starts Phase 1 (agent speaks first)
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
# Launcher: auto-starts Phase 1 (agent speaks first via CLI PTY inject)
|
||||
mkdir -p "${HOME}/.local/bin" "${HOME}/.local/share/hermes-client-onboarding"
|
||||
if [[ -f "$staging/scripts/start-onboarding.sh" ]]; then
|
||||
cp -f "$staging/scripts/start-onboarding.sh" "${HOME}/.local/bin/hermes-client-onboarding"
|
||||
chmod +x "${HOME}/.local/bin/hermes-client-onboarding"
|
||||
log "Launcher → ~/.local/bin/hermes-client-onboarding (agent fala primeiro)"
|
||||
fi
|
||||
if [[ -f "$staging/scripts/auto_kickoff_cli.py" ]]; then
|
||||
cp -f "$staging/scripts/auto_kickoff_cli.py" "${HOME}/.local/share/hermes-client-onboarding/auto_kickoff_cli.py"
|
||||
cp -f "$staging/scripts/auto_kickoff_cli.py" "${HOME}/.hermes/skills/${SKILL_NAME}/scripts/auto_kickoff_cli.py"
|
||||
chmod +x "${HOME}/.local/share/hermes-client-onboarding/auto_kickoff_cli.py"
|
||||
fi
|
||||
|
||||
# Codex / agents (optional)
|
||||
if [[ -d "${HOME}/.codex" ]] || need_cmd codex; then
|
||||
|
|
@ -210,21 +220,26 @@ pick_conductor() {
|
|||
}
|
||||
|
||||
launch_hermes_onboarding() {
|
||||
# Auto-start: TUI + -q submits first turn; session stays interactive.
|
||||
export HERMES_ONBOARD_KICKOFF="$KICKOFF_MSG"
|
||||
export HERMES_ONBOARD_SKILL="$SKILL_NAME"
|
||||
export HERMES_TUI_QUERY="$KICKOFF_MSG"
|
||||
export HERMES_TUI_SKILLS="$SKILL_NAME"
|
||||
if [[ -x "${HOME}/.local/bin/hermes-client-onboarding" ]]; then
|
||||
if [[ -r /dev/tty ]]; then
|
||||
exec "${HOME}/.local/bin/hermes-client-onboarding" </dev/tty
|
||||
exec "${HOME}/.local/bin/hermes-client-onboarding" </dev/tty >/dev/tty 2>/dev/tty
|
||||
else
|
||||
exec "${HOME}/.local/bin/hermes-client-onboarding"
|
||||
fi
|
||||
fi
|
||||
# Fallback without launcher binary (-q only works on `hermes chat`)
|
||||
# Fallback: classic CLI + PTY inject script if present
|
||||
local auto_py="${HOME}/.hermes/skills/${SKILL_NAME}/scripts/auto_kickoff_cli.py"
|
||||
if [[ -f "$auto_py" ]] && command -v python3 >/dev/null 2>&1; then
|
||||
if [[ -r /dev/tty ]]; then
|
||||
exec hermes chat --tui -s "$SKILL_NAME" -q "$KICKOFF_MSG" </dev/tty
|
||||
else
|
||||
exec hermes chat --tui -s "$SKILL_NAME" -q "$KICKOFF_MSG"
|
||||
exec python3 "$auto_py" </dev/tty >/dev/tty 2>/dev/tty
|
||||
fi
|
||||
exec python3 "$auto_py"
|
||||
fi
|
||||
die "launcher missing — re-run install or: hermes chat --cli -s ${SKILL_NAME}"
|
||||
}
|
||||
|
||||
launch_conductor() {
|
||||
|
|
|
|||
|
|
@ -78,11 +78,11 @@ Preferred launch (agent auto-starts):
|
|||
|
||||
```bash
|
||||
hermes-client-onboarding
|
||||
# or:
|
||||
hermes chat --tui -s hermes-client-onboarding -q "Inicie AGORA o onboarding…"
|
||||
# uses classic CLI + auto-kickoff inject (reliable)
|
||||
# TUI (optional): HERMES_ONBOARD_USE_TUI=1 hermes-client-onboarding
|
||||
```
|
||||
|
||||
Plain `hermes chat -s hermes-client-onboarding` without `-q` waits for user input — avoid that for demos.
|
||||
Plain `hermes chat -s hermes-client-onboarding` without kickoff waits for user input — avoid that for demos.
|
||||
|
||||
### Phase 1 — Context & Goals
|
||||
|
||||
|
|
|
|||
127
skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py
Executable file
127
skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Spawn `hermes chat --cli -s <skill>` and inject the kickoff as first user message.
|
||||
|
||||
Hermes only runs a model turn after a user message. The TUI startup-query path
|
||||
races session creation (~4s) and often silently skips. This classic-CLI path
|
||||
waits for a ready prompt, sends the kickoff once, then hands the TTY to the user.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def main() -> int:
|
||||
skill = os.environ.get("HERMES_ONBOARD_SKILL", "hermes-client-onboarding")
|
||||
kickoff = os.environ.get(
|
||||
"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. Português brasileiro.",
|
||||
)
|
||||
if not kickoff.endswith("\n"):
|
||||
kickoff += "\n"
|
||||
|
||||
argv = ["hermes", "chat", "--cli", "-s", skill]
|
||||
# Extra args after --
|
||||
if len(sys.argv) > 1:
|
||||
argv.extend(sys.argv[1:])
|
||||
|
||||
pid, master = pty.fork()
|
||||
if pid == 0:
|
||||
os.execvp(argv[0], argv)
|
||||
|
||||
# Parent: relay I/O; inject kickoff once after session looks ready.
|
||||
sent = False
|
||||
buf = b""
|
||||
start = time.time()
|
||||
inject_after = 1.5 # min wait for banner
|
||||
deadline = start + 45.0
|
||||
|
||||
try:
|
||||
while True:
|
||||
timeout = 0.2
|
||||
r, _, _ = select.select([master, sys.stdin], [], [], timeout)
|
||||
now = time.time()
|
||||
|
||||
if master in r:
|
||||
try:
|
||||
data = os.read(master, 8192)
|
||||
except OSError:
|
||||
data = b""
|
||||
if not data:
|
||||
break
|
||||
os.write(sys.stdout.fileno(), data)
|
||||
buf += data
|
||||
if len(buf) > 20000:
|
||||
buf = buf[-10000:]
|
||||
|
||||
if sys.stdin in r:
|
||||
try:
|
||||
data = os.read(sys.stdin.fileno(), 8192)
|
||||
except OSError:
|
||||
data = b""
|
||||
if not data:
|
||||
# stdin closed — keep agent until it exits
|
||||
pass
|
||||
else:
|
||||
os.write(master, data)
|
||||
|
||||
if not sent and now >= start + inject_after:
|
||||
lower = buf.lower()
|
||||
ready_markers = (
|
||||
b"ready",
|
||||
b"session:",
|
||||
b"try ",
|
||||
b"welcome",
|
||||
b"type your message",
|
||||
b"\n> ",
|
||||
b"\n❯",
|
||||
b"\nprompt",
|
||||
)
|
||||
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
|
||||
|
||||
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)
|
||||
if wpid == pid:
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
try:
|
||||
os.kill(pid, 2)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
return 130
|
||||
finally:
|
||||
try:
|
||||
os.close(master)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Blocking wait if loop broke on EOF from master
|
||||
try:
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
except ChildProcessError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,23 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# Launch Hermes with hermes-client-onboarding and auto-start Phase 1
|
||||
# (agent speaks first — does not wait for the user to say "oi").
|
||||
# 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).
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
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.}"
|
||||
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.}"
|
||||
|
||||
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"
|
||||
|
||||
if ! command -v hermes >/dev/null 2>&1; then
|
||||
echo "error: hermes not on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Must use `hermes chat` subcommand: -q is not a top-level flag.
|
||||
# With --tui, -q becomes HERMES_TUI_QUERY — first turn auto-submits, session stays interactive.
|
||||
if [[ -t 0 && -t 1 ]]; then
|
||||
exec hermes chat --tui -s "$SKILL_NAME" -q "$KICKOFF" "$@"
|
||||
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
|
||||
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
|
||||
|
||||
# Non-TTY fallback (CI/scripts): one-shot only
|
||||
# Reliable path: classic CLI + inject first user message
|
||||
if [[ -n "$AUTO_PY" ]] && command -v python3 >/dev/null 2>&1; then
|
||||
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
|
||||
exec hermes chat -s "$SKILL_NAME" -Q -q "$KICKOFF" "$@"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue