fix(install): survive pip-less uv-created Hermes venvs; stop hiding errors
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 30 Aug 2026 00:53:57 +0000 (03:53 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 30 Aug 2026 00:53:57 +0000 (03:53 +0300)
Found installing on a fresh Debian 13 machine (Hermes installed via its
own uv-based install.sh):

- Root cause: 'uv venv' does not seed pip into the Hermes runtime venv,
  so '$PYTHON -m pip install' died with 'No module named pip' and the
  installer's suggested manual pip command would have failed the same
  way. Dependency installation now chains: pip -> ensurepip-bootstrapped
  pip -> 'uv pip install --python' (uv probed on PATH and at
  $HERMES_HOME/bin/uv, covering non-login SSH shells with minimal PATH).
- No more silent failures: every fallible step (deps, model preload,
  hermes config set, plugin validation) captured stderr behind
  2>/dev/null, making the remote failure undebuggable. All of them now
  capture combined output into DIAG_DIR (cleaned via EXIT trap) and
  print a show_log_tail excerpt on failure; failure guidance names
  commands that actually work on a pip-less host (uv, ensurepip).
- Docs: Documentation/index.org step 2 describes the install chain;
  AGENTS.org gains the uv-venv-has-no-pip and no-silent-2>/dev/null
  pitfalls and the tooling note describes the new chain.

Verified with a stubbed-environment ad-hoc harness (20/20): pip-less
uv venv + broken ensurepip + uv present (exact remote repro) installs
via uv; pip-present venv uses pip; total failure prints captured
diagnostics and working manual commands. Canonical suite green.

AGENTS.org
Documentation/index.org
Install Retinue

index 2307863..3d286a1 100644 (file)
@@ -203,10 +203,11 @@ Never commit the generated =*.html= — they are gitignored.
 - *Python on Debian 13 is PEP 668.* System pip is blocked. The installer
   deliberately installs deps into the Python that runs Hermes
   (=PYTHON= env override, =$HERMES_HOME/hermes-agent/venv/bin/python3=,
-  or the =hermes= launcher shebang), and falls back to
-  =--break-system-packages= only inside that interpreter. The standalone
-  tests use the project =.venv=. Do not pip-install into the system
-  Python from this project.
+  or the =hermes= launcher shebang). That interpreter is a uv-created venv
+  without pip (see pitfall below), so dependency installation tries pip,
+  then =ensurepip=-bootstrapped pip, then =uv= from =PATH= or
+  =$HERMES_HOME/bin/uv=. Do not pip-install into the system Python from
+  this project.
 - *Filenames with spaces.* Everything under =Tools/= contains spaces —
   quote paths in shell commands.
 - *The =Tools/Open with IntelliJ IDEA= helper* opens this tree as an
@@ -267,6 +268,21 @@ Never commit the generated =*.html= — they are gitignored.
   Hermes at =~/.hermes/bin/uv=), then falls back to the Hermes runtime
   venv. All =Tools/= scripts source it — never open-code venv handling
   in a Tools script.
+- *Pitfall: the Hermes runtime venv has no pip.* Hermes's own installer
+  creates =~/.hermes/hermes-agent/venv= with =uv venv=, which does not seed
+  pip — =venv/bin/python3 -m pip= dies with =No module named pip= (found
+  2026-08-30 installing Retinue on a fresh Debian 13 machine: both pip
+  attempts failed, and the suggested manual =pip install= fix would have
+  failed the same way). The installer now chains pip → ensurepip → uv
+  (=$HERMES_HOME/bin/uv=, probed explicitly because non-login shells have a
+  minimal PATH) and prints captured attempt output on failure instead of
+  hiding it behind =2>/dev/null=. Keep every one of those fallbacks.
+- *Pitfall: silent failure via =2>/dev/null=.* The 2026-08-30 install
+  failure was undebuggable because the installer discarded stderr. Every
+  fallible step (deps, model preload, =hermes config set=, plugin
+  validation) now captures combined output into a temp dir (=DIAG_DIR=,
+  cleaned by an EXIT trap) and prints a =show_log_tail= excerpt on
+  failure. Never reintroduce bare =2>/dev/null= on a step that can fail.
 - *Pitfall: gateway restart on the remote.* =systemctl --user restart
   hermes-gateway.service= can leave the unit in =activating= when an old
   gateway process (started with =gateway run --replace= outside systemd)
index 38dba61..c1c1ace 100644 (file)
@@ -69,7 +69,10 @@ The installer performs four steps:
 1. Detects the active Hermes home from the =HERMES_HOME= environment variable
    or falls back to =$HOME/.hermes=.
 2. Resolves the Python interpreter that Hermes uses and installs =sqlite-vec=
-   and =model2vec= there if they are missing.
+   and =model2vec= there if they are missing — via pip when present, via
+   =ensurepip=-bootstrapped pip otherwise, and finally via =uv= (Hermes
+   bundles one at =~/.hermes/bin/uv=), which also covers uv-created Hermes
+   venvs that ship without pip.
 3. Copies the Retinue source into =$HERMES_HOME/plugins/retinue/=, preserving
    any existing runtime data.
 4. Pre-downloads the embedding model
index fb17d83..c0db46a 100755 (executable)
 
 set -euo pipefail
 
+# Diagnostics: every step that can fail captures its combined output here and
+# prints it on failure. Never hide stderr behind 2>/dev/null — a silent
+# failure on an unfamiliar host is undebuggable (found 2026-08-30: a pip-less
+# Hermes venv failed the install with zero visible reason).
+DIAG_DIR="$(mktemp -d)"
+trap 'rm -rf "$DIAG_DIR"' EXIT
+
+# Print the last lines of a captured log, tolerating tqdm-style \r progress
+# spam (model downloads) that would otherwise arrive as one giant line.
+show_log_tail() { # show_log_tail <file> <n-lines>
+    tr '\r' '\n' <"$1" | tail -n "${2:-15}" | sed 's/^/    /'
+}
+
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 SOURCE_DIR="$SCRIPT_DIR"
 
@@ -135,6 +148,14 @@ done
 
 # Ensure Python dependencies are available in the target interpreter.
 # Hermes itself runs with this Python, so the deps must be importable there.
+#
+# The Hermes runtime venv is created by uv, which does NOT seed pip into it —
+# "$PYTHON -m pip" fails there with "No module named pip". On bare Debian 13
+# hosts ensurepip is also missing (no python3.X-venv apt package), so the
+# reliable path is the uv binary Hermes itself installed at
+# $HERMES_HOME/bin/uv. Try every available installer in order and capture
+# each attempt's output so a total failure can show real diagnostics
+# instead of failing silently.
 ensure_python_deps() {
     local missing=""
     if ! "$PYTHON" -c "import sqlite_vec" 2>/dev/null; then
@@ -148,21 +169,63 @@ ensure_python_deps() {
     fi
 
     echo "Missing Python dependencies:$missing"
-    echo "Installing with $PYTHON -m pip ..."
 
-    if "$PYTHON" -m pip install $missing 2>/dev/null; then
-        echo "Dependencies installed successfully."
-        return 0
+    local logdir attempt=0
+    logdir="$(mktemp -d)"
+
+    try_install() { # try_install <label> <command...>
+        local label="$1"; shift
+        attempt=$((attempt + 1))
+        echo "Installing with $label ..."
+        if "$@" >"$logdir/attempt-$attempt.log" 2>&1; then
+            echo "Dependencies installed successfully ($label)."
+            rm -rf "$logdir"
+            return 0
+        fi
+        return 1
+    }
+
+    if "$PYTHON" -m pip --version >/dev/null 2>&1; then
+        try_install "$PYTHON -m pip" \
+            "$PYTHON" -m pip install $missing && return 0
+        try_install "$PYTHON -m pip --break-system-packages" \
+            "$PYTHON" -m pip install --break-system-packages $missing && return 0
+    else
+        echo "  ($PYTHON has no pip module — typical for uv-created Hermes venvs)"
+        # Bootstrap pip via ensurepip, then use it. Fails harmlessly on hosts
+        # without the python3.X-venv apt package (no bundled pip wheel).
+        if "$PYTHON" -m ensurepip --upgrade >"$logdir/ensurepip.log" 2>&1; then
+            try_install "$PYTHON -m pip (bootstrapped via ensurepip)" \
+                "$PYTHON" -m pip install $missing && return 0
+        fi
     fi
 
-    if "$PYTHON" -m pip install --break-system-packages $missing 2>/dev/null; then
-        echo "Dependencies installed successfully (with --break-system-packages)."
-        return 0
+    # uv can install into any interpreter without pip. Hermes bundles its own
+    # uv at $HERMES_HOME/bin/uv, and installers often run over non-login SSH
+    # where PATH is minimal — probe both.
+    local UV
+    UV="$(command -v uv 2>/dev/null || true)"
+    [[ -z "$UV" && -x "$HERMES_HOME/bin/uv" ]] && UV="$HERMES_HOME/bin/uv"
+    if [[ -n "$UV" ]]; then
+        try_install "$UV pip install --python $PYTHON" \
+            "$UV" pip install --python "$PYTHON" $missing && return 0
+        try_install "$UV pip install --break-system-packages --python $PYTHON" \
+            "$UV" pip install --break-system-packages --python "$PYTHON" $missing && return 0
     fi
 
     echo "ERROR: Failed to install dependencies."
-    echo "       Install them manually:"
-    echo "         $PYTHON -m pip install --break-system-packages sqlite-vec model2vec"
+    local f
+    for f in "$logdir"/*.log; do
+        [[ -s "$f" ]] || continue
+        echo "--- output of $(basename "$f" .log) (last lines):"
+        tail -n 15 "$f" | sed 's/^/    /'
+    done
+    rm -rf "$logdir"
+    echo "       Install them manually with uv (bundled with Hermes):"
+    echo "         $HERMES_HOME/bin/uv pip install --python $PYTHON sqlite-vec model2vec"
+    echo "       or bootstrap pip into the interpreter first:"
+    echo "         $PYTHON -m ensurepip --upgrade"
+    echo "         $PYTHON -m pip install sqlite-vec model2vec"
     return 1
 }
 
@@ -175,16 +238,20 @@ ensure_python_deps
 preload_embedding_model() {
     echo "Pre-downloading embedding model (minishlab/potion-multilingual-128M)..."
     export HF_HOME="$PLUGIN_DIR/.hf-cache"
+    local log="$DIAG_DIR/preload-model.log"
     if "$PYTHON" -c "
 import sys
 sys.path.insert(0, '$PLUGIN_DIR/src')
 from retinue.memory import get_model
 get_model()
 print('  Embedding model ready.')
-" 2>/dev/null; then
+" >"$log" 2>&1; then
+        show_log_tail "$log" 3
         echo "  Model cached at $PLUGIN_DIR/.hf-cache"
     else
         echo "  Warning: could not pre-download the embedding model."
+        echo "  --- last lines of output:"
+        show_log_tail "$log" 20
         echo "           It will download on first use inside Hermes."
         echo "           (Check network / Hugging Face connectivity.)"
     fi
@@ -208,10 +275,12 @@ fi
 
 if [[ -n "$HERMES_CLI" ]]; then
     echo "Setting memory.provider to retinue (via $HERMES_CLI)..."
-    if "$HERMES_CLI" config set memory.provider retinue 2>/dev/null; then
+    if "$HERMES_CLI" config set memory.provider retinue >"$DIAG_DIR/config-set.log" 2>&1; then
         echo "  memory.provider set to retinue."
     else
         echo "  Warning: could not set memory.provider automatically."
+        echo "  --- hermes config set output:"
+        show_log_tail "$DIAG_DIR/config-set.log"
         echo "           Set it manually in $HERMES_HOME/config.yaml:"
         echo "             memory:"
         echo "               provider: retinue"
@@ -231,12 +300,16 @@ p = RetinueMemoryProvider()
 print('name:', p.name)
 print('available:', p.is_available())
 print('schemas:', [s['name'] for s in p.get_tool_schemas()])
-" 2>/dev/null; then
+" >"$DIAG_DIR/validate.log" 2>&1; then
+    show_log_tail "$DIAG_DIR/validate.log"
     echo "Plugin validation passed."
 else
     echo "Plugin validation failed. Dependencies may be missing."
+    echo "--- validation output:"
+    show_log_tail "$DIAG_DIR/validate.log" 25
     echo "Install them with:"
-    echo "  $PYTHON -m pip install sqlite-vec model2vec"
+    echo "  $HERMES_HOME/bin/uv pip install --python $PYTHON sqlite-vec model2vec"
+    echo "  # or: $PYTHON -m pip install sqlite-vec model2vec"
     echo "The embedding model (~500 MB) will download on first use."
     exit 1
 fi