diff --git a/README.md b/README.md index 83ee3bd..8c0e0a0 100644 --- a/README.md +++ b/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 diff --git a/install.sh b/install.sh index 4435576..83d02e8 100755 --- a/install.sh +++ b/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 2>/dev/tty else exec "${HOME}/.local/bin/hermes-client-onboarding" fi fi - # Fallback without launcher binary (-q only works on `hermes chat`) - if [[ -r /dev/tty ]]; then - exec hermes chat --tui -s "$SKILL_NAME" -q "$KICKOFF_MSG" /dev/null 2>&1; then + if [[ -r /dev/tty ]]; then + exec python3 "$auto_py" /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() { diff --git a/skill/hermes-client-onboarding/SKILL.md b/skill/hermes-client-onboarding/SKILL.md index 775765d..69c271b 100644 --- a/skill/hermes-client-onboarding/SKILL.md +++ b/skill/hermes-client-onboarding/SKILL.md @@ -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 diff --git a/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py b/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py new file mode 100755 index 0000000..c81063c --- /dev/null +++ b/skill/hermes-client-onboarding/scripts/auto_kickoff_cli.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Spawn `hermes chat --cli -s ` 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()) diff --git a/skill/hermes-client-onboarding/scripts/start-onboarding.sh b/skill/hermes-client-onboarding/scripts/start-onboarding.sh index 5b1f017..e90f7b1 100755 --- a/skill/hermes-client-onboarding/scripts/start-onboarding.sh +++ b/skill/hermes-client-onboarding/scripts/start-onboarding.sh @@ -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/null 2>&1; then + 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 exec hermes chat -s "$SKILL_NAME" -Q -q "$KICKOFF" "$@"