--- name: hermes-restart description: "Reload or restart the Hermes gateway on a remote VPS over SSH with the native v0.22.5 lifecycle: gateway restart --detach, then gateway ensure --json until it reports a ready gateway with a new instance. Use after a config or .env change, or when the gateway is stuck or down. Works from any AI agent (Hermes, Claude Code, Codex, Cursor, Gemini CLI)." --- # /hermes-restart — reload the gateway the native way (SSH-first) You are the engineer reloading the Hermes gateway for the user. The agent (Hermes, Codex, Claude Code, Cursor, Gemini CLI — whichever ran you) is talking to a remote VPS over SSH as root. This is the user-facing skill for "reload / restart the gateway", and it documents the canonical lifecycle every other hstack skill uses (`gw_reload` and `gw_ready` in Step 0). Hermes v0.22.5 does the hard parts itself: stale pid/lock files, other profiles, drain of live turns and the service manager are all handled by `gateway restart` and `gateway ensure`. This skill therefore **changes no files, deletes nothing and kills nothing**. It asks the native commands, reads their answer, and reports it. If they refuse, it says why; it does not work around the refusal. **Where commands run.** Every `bash` block below runs on the agent's LOCAL machine (possibly macOS, bash 3.2) and reaches the VPS through the `remote` helper from Step 0, which wraps `ssh` and puts `~/.local/bin` (where `hermes` lives) on the remote PATH. Run all blocks in ONE shell session: later steps call the Step 0 functions. **Honest picture.** Everything below is checked against the Hermes v0.22.5 source (see "Verified against Hermes v0.22.5"). Nothing was run against a real Hermes host; the offline tests drive these exact blocks against a stub whose command *syntax* is the real v0.22.5 argparse and whose *behaviour* is an assumption. --- ## How the native lifecycle works (read once) `gw_reload` runs `hermes -p default gateway restart --detach` and then requires `gw_ready`, which is `gateway ensure --json --timeout 60` exiting 0 with `"state":"ready"` and a different `instance_id` than before. - `gateway restart` restarts the installed systemd/launchd service. With no service it stops the profile's gateway and starts the replacement; `--detach` starts that replacement in the background the way `ensure` does, so an ssh call cannot hang (`hermes_cli/gateway.py:5454-5536`, `hermes_cli/gateway_supervised_restart.py:140-151`, `hermes_cli/gateway_runtime_start.py:86-97`). A restart is drain-aware and can wait for in-flight turns (`agent.restart_after_turn_timeout`, default 1800 s: `hermes_cli/config_defaults.py:123`); do not interrupt the ssh call. - With live sessions it asks y/N on a terminal and **refuses with exit 1 without one**. `-y`/`--yes`/`--interrupt` interrupts them (`hermes_cli/gateway_lifecycle_guards.py:81-129`, `hermes_cli/subcommands/gateway.py:146-155`). The helper never adds `-y` on its own: set `GW_INTERRUPT=1` only after the user agreed that interrupting live chats, cron jobs and background delegations is acceptable. - `gateway ensure` never installs a service; it starts an installed one or spawns an unmanaged daemon, and exits 0 only when the gateway is ready. Exit codes: 0 ready, 2 invalid invocation, 3 incompatible, 4 authorization or profile mismatch, 5 deadline or still starting, 6 draining or update-paused, 7 inaccessible or conflicting supervisor (`hermes_cli/subcommands/gateway.py:122-131`, `hermes_cli/gateway_runtime_cli.py:8-37`, `hermes_cli/gateway_runtime.py:390-495`). `instance_id` is a fresh uuid per gateway process (`gateway/run_runtime.py:144`), which is how `gw_reload` notices a restart that silently did nothing. - `gateway status` prints `✗ Gateway is not running` when stopped (exit 0), so its text is **never** a health check (`hermes_cli/gateway.py:5636`). It is shown to the user as information only. - A restarted gateway process loads `~/.hermes/.env` at start (`gateway/run.py:1583-1585`; it is also reloaded per turn outside multiplex mode, `:1588-1600`), so `restart` does re-read `.env`; config.yaml and MCP changes need the reload too. - The recipe assumes the exclusive default profile and its one host gateway; named-profile and shared-host deployments are out of scope. There is no `--daemon` flag at all; the foreground command is `gateway run`. --- ## Before you start — gather (ask once, in one batch) Required from the user (skip any already in env): | Variable | What it is | How to get it | |----------|------------|---------------| | `$VPS_IP` | IP or hostname of the VPS running Hermes | Their hosting dashboard | | `$VPS_USER` | SSH user, usually `root` | Their hosting dashboard | | `$GW_INTERRUPT` (optional) | `1` = interrupt live sessions (adds `-y`). Leave unset unless the user said so | Ask: "chats are running; interrupt them?" | | `$INSTALL_SERVICE` (optional) | `1` = also install the supervised service so the gateway survives reboots (Step 4). Persistent change: needs the user's explicit yes | Ask only if they want it to survive reboots | Confirm SSH access before doing anything: ```bash ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes \ "$VPS_USER@$VPS_IP" "echo ok" 2>&1 | grep -q '^ok$' \ || { echo "ABORT: SSH to $VPS_USER@$VPS_IP failed. Run /setup-ssh-keys first. Nothing was changed."; exit 1; } ``` --- ## Step 0 — helpers (define first, so nothing can fail before they exist) ```bash # Run every block of this skill in ONE shell session: later steps call these functions. # If your harness starts a fresh shell per command, re-run this block first. SKILL_NAME=hermes-restart # Non-login ssh shells miss ~/.local/bin, where the Hermes installer puts `hermes`. remote() { ssh -o BatchMode=yes "$VPS_USER@$VPS_IP" "export PATH=\"\$HOME/.local/bin:\$PATH\"; $*"; } # gw_ready [SECONDS]: native readiness. Succeeds only when `gateway ensure --json` exits 0 and reports "state":"ready" # (the transient pending/draining codes 5 and 6 are retried twice). It never greps `gateway status` text, because the stopped # gateway prints "✗ Gateway is not running", which contains "running". gw_ready() { remote "T=${1:-60} bash -s" <<'REMOTE' n=0 while :; do out=$(hermes -p default gateway ensure --json --timeout "$T") && rc=0 || rc=$? [ "$rc" = 0 ] && break n=$((n + 1)) { [ "$rc" = 5 ] || [ "$rc" = 6 ]; } && [ "$n" -lt 3 ] || { printf '%s\n' "$out" >&2; exit 1; } sleep 3 done case $out in *'"state":"ready"'*) printf '%s\n' "$out" ;; *) printf '%s\n' "$out" >&2; exit 1 ;; esac REMOTE } # gw_reload: restart the default-profile gateway natively and require a ready gateway with a NEW instance_id. # `--detach` keeps `restart` from running the gateway inside the ssh shell when no service is installed. With live # sessions and no terminal, restart refuses; GW_INTERRUPT=1 adds -y and interrupts them deliberately (never set by default). # Side effect: the pre-restart `ensure` read that captures the old instance_id STARTS a gateway when none is running (ensure never # installs a service, but it does start one). A reload implies the gateway should run, so that is accepted; read-only skills never call gw_reload. gw_reload() { local old new out interrupt interrupt="" if [ "${GW_INTERRUPT:-0}" = 1 ]; then interrupt=1; fi # Keep an endpoint identity even when ensure exits 6 (draining/restart_wait). out=$(remote 'hermes -p default gateway ensure --json --timeout 10 2>/dev/null') || : old=$(printf '%s\n' "$out" | sed -n 's/.*"instance_id":"\([^"]*\)".*/\1/p') remote "hermes -p default gateway restart --detach${interrupt:+ -y}" \ || { echo "gw_reload: gateway restart failed or was refused (live sessions? re-run with GW_INTERRUPT=1 only if interrupting them is acceptable)." >&2; return 1; } out=$(gw_ready) || return 1 new=$(printf '%s' "$out" | sed -n 's/.*"instance_id":"\([^"]*\)".*/\1/p') if [ -n "$old" ] && [ "$new" = "$old" ]; then echo "gw_reload: the gateway instance did not change, so it was not restarted." >&2; return 1; fi printf '%s\n' "$out" } ``` This skill edits no file, so it has no `snapshot`/`rollback`/`finish`. A failure leaves the gateway as the native commands left it and the skill reports it (Step 6). --- ## Step 1 — verify Hermes v0.22.5 is reachable on the VPS ```bash HERMES_BANNER=$(remote ' set -e command -v hermes >/dev/null 2>&1 \ || { echo "FAIL: hermes not found on the VPS host PATH; container-only installations are outside this procedure"; exit 1; } hermes -p default --version ') || { echo "ABORT: Hermes is not installed on the VPS host account (container-only installs are unsupported). Run /hermes-install first. Nothing was changed."; exit 1; } printf '%s\n' "$HERMES_BANNER" | grep -Eq '(^|[[:space:]])v?0[.]22[.]5([[:space:]]|$)' || { echo "ABORT: this procedure is source-verified for Hermes 0.22.5 only (got: $HERMES_BANNER). Nothing was changed."; exit 1; } ``` Expected: a banner naming Hermes `0.22.5` (the version pinned as `HERMES_VERSION` in `VERSIONS.txt`). The `whoami; hostname` check from `CLAUDE.md` applies if you ssh in by hand: a hex hostname means you are inside a container, not on the host. --- ## Step 2 — look first (read-only, informational) A restart is not skipped because the gateway "looks healthy": the user asked for a reload, and no text from `gateway status` can prove health. Show what the native tools say, and keep it as information: ```bash echo "--- gateway status (informational text, not a health verdict) ---" remote 'hermes -p default gateway status' || echo "(gateway status exited $?)" echo "--- last gateway log lines ---" remote 'hermes -p default logs gateway -n 30' || echo "(logs exited $?)" ``` If the user only wants to know whether the agent is fine, stop here and use `/hermes-status`; it reads more and starts nothing. --- ## Step 3 — DRY RUN preview (always show before touching anything) ```bash LIVE_NOTE="the restart REFUSES and nothing changes (set GW_INTERRUPT=1 to interrupt them)" GW_RESTART_FLAG="" if [ "${GW_INTERRUPT:-0}" = 1 ]; then LIVE_NOTE="they WILL be interrupted (GW_INTERRUPT=1)" GW_RESTART_FLAG=1 fi cat <