Files
hermes-skills/hermes-source-updates/SKILL.md
T

11 KiB

name, description, version, author, license, platforms, metadata
name description version author license platforms metadata
hermes-source-updates Use when hermes update stashes source or cleanup is needed. 1.1.0 Hermes Agent MIT
linux
hermes
tags related_skills
hermes
update
git
stash
install
source
hermes-agent
hermes-profile-management
ask-claude

Hermes Source Updates & Autostash Cleanup

Git-installed Hermes lives at ~/.hermes/hermes-agent. hermes update pulls that repo. Uncommitted source edits get auto-stashed first.

What "local changes are stashed" means

  • Is: temporary git stash of edits inside the Hermes source tree so git pull can run.
  • Is not: config, .env, skills, sessions, memory, profiles, cron. Those live under ~/.hermes/ outside the git tree and are covered by the update's pre-update snapshot, not by stash messages.

Docs: https://hermes-agent.nousresearch.com/docs/getting-started/updating/

Interactive update: stash → pull → prompt to restore. Non-interactive (/update, desktop, --yes): updates.non_interactive_local_changes = stash (default, auto-restore) or discard.

Inspect stashes (always start here)

cd ~/.hermes/hermes-agent
git status -sb
git stash list
git stash show --stat 'stash@{N}'
git stash show -p 'stash@{N}' | head -200

Autostash names look like: hermes-update-autostash-YYYYMMDD-HHMMSS.

Noise vs keep

Signal Treat as
Only package-lock.json / "peer": true churn Noise → drop
WIP name + lockfile-only Noise → drop
Real .py / SOUL.md / gateway / voice patches Keep until proven obsolete
Mixed lockfile + real code Keep whole stash (do not surgical-extract unless user asks)

Confirm identity before drop:

git stash list
git stash show --stat 'stash@{N}'   # must match expected noise
git stash drop 'stash@{N}'

After any drop, indexes renumber. Re-run git stash list before the next drop.

Is an old stash already on main?

Do not use git diff main stash@{N} for this. Old stash commits diverge from current main across the whole tree (thousands of files) and look "non-empty" even when the patch intent is gone or unrelated.

Use the patch of the stash:

git stash show -p 'stash@{N}' > /tmp/stashN.patch

# Optional: strip lockfiles before checks
python3 - <<'PY'
from pathlib import Path
p = Path('/tmp/stashN.patch').read_text(errors='replace')
parts = p.split('diff --git ')
keep = []
for part in parts:
    if not part.strip():
        continue
    head = part.split('\n', 1)[0]
    if 'package-lock.json' in head:
        continue
    keep.append('diff --git ' + part)
Path('/tmp/stashN-code.patch').write_text(''.join(keep))
print('wrote code-only patch')
PY

cd ~/.hermes/hermes-agent
git apply --reverse --check /tmp/stashN-code.patch   # exit 0 → content already in tree
git apply --check /tmp/stashN-code.patch              # exit 0 → clean apply on current tree

Interpretation:

  • reverse-check succeeds → patch content already present → safe to drop as obsolete
  • reverse and forward both fail → tree drifted; patch is unique or outdated form → keep unless user chooses discard
  • forward succeeds → still local-only; keep or re-apply intentionally

Also grep current tree for distinctive symbols from the stash.

Safe cleanup workflow

  1. List + --stat every stash.
  2. Drop pure lockfile/WIP noise only (confirm --stat first).
  3. Hold intentional patches (SOUL, hang fixes, TTS timeouts, delegate/SearXNG local work).
  4. For mixed/old stashes: code-only patch check above; only drop if reverse-check proves already upstream.
  5. Verify: git stash list + git status -s (expect clean aside from known untracked like build/).

When user says "ask Claude to clean up" / analyze stashes

Claude on 10.0.0.28 has no automatic access to Hermes-MAIN. Two modes:

  1. Paste mode (default): inventory (stash list, --stat, short -p) into ask-claude. Ask for keep/drop + exact commands. You execute locally.
  2. SSH mode (only when user says Claude may SSH in): export patches on Hermes-MAIN, authorize Claude host key if needed, then let Claude inspect live tree.
# On Hermes-MAIN (10.0.0.42) — export for Claude
mkdir -p /tmp/hermes-stashes
cd ~/.hermes/hermes-agent
for i in 0 1 2; do
  git stash show -p "stash@{$i}" > "/tmp/hermes-stashes/stash${i}.patch" 2>/dev/null || true
  git stash show --stat "stash@{$i}" > "/tmp/hermes-stashes/stash${i}.stat" 2>/dev/null || true
done

# From 10.0.0.28 (Claude host key is vera-ai):
ssh -i ~/.ssh/vera-ai -o StrictHostKeyChecking=accept-new [email protected] \
  'git -C /home/n8n/.hermes/hermes-agent stash list; ls /tmp/hermes-stashes/'

If Permission denied, append vera-ai.pub to Hermes-MAIN ~/.ssh/authorized_keys (user must allow). Tell Claude: analysis only unless operator authorized apply/drop.

Detail: references/update-stash-cleanup.md.

Re-apply intentional stashes (after Claude KEEP+RE-APPLY)

Do not git stash pop blindly. Prefer git stash apply 'stash@{N}', resolve conflicts, leave as unstaged working-tree mods (so next hermes update will stash them again intentionally).

Pre-flight: is the intent already upstream?

Before fighting conflicts, grep current main for the behavior, not the old symbol names:

Old stash intent Upstream equivalent (check first)
inline _DaemonThreadPoolExecutor in tool_executor.py tools/daemon_pool.pyDaemonThreadPoolExecutor
gateway/run.py _voice_mode_getter wire often already near _sync_voice_mode_state_to_adapter
old SearXNG in web_tools.py + refresh_delegate_schema newer SearXNG provider + MCP inherit helpers — usually DISCARD
load_soul_identity=True on child agents kwarg may be gone — keep stash for intent, do not blind-apply
rg -n "DaemonThreadPoolExecutor|daemon_pool" agent/tool_executor.py tools/daemon_pool.py
rg -n "_voice_mode_getter" gateway/run.py gateway/platforms/base.py
rg -n "load_soul_identity" -g '*.py'

When git stash apply fails (heavy upstream drift)

If git stash apply produces conflicts on most files, do NOT fight through merge markers. Extract the patch, inspect each file's changes, check what's already upstream, and apply only the missing pieces fresh with the patch tool:

# 1. Extract the full patch
git stash show -p 'stash@{N}' > /tmp/stash.patch

# 2. Split per-file and inspect each change
python3 -c "
patch = open('/tmp/stash.patch').read()
parts = patch.split('diff --git ')
for part in parts:
    if not part.strip():
        continue
    head = part.split('\n', 1)[0]
    f = head.split()[-1].replace('b/', '')
    print(f'=== {f} ===')
    for line in part.split('\n'):
        if line.startswith('+') and not line.startswith('+++'):
            print(line)
        elif line.startswith('-') and not line.startswith('---'):
            print(line)
    print()
"

# 3. For each file, grep current tree to see what's already upstream
#    (e.g. grep for the behavior, not the exact old line numbers)

# 4. Apply only the missing changes with the patch tool
#    (patch mode='replace' on each file, not git stash apply)

# 5. Drop the stash once all changes are applied
git stash drop 'stash@{N}'

This avoids merge conflicts entirely and produces a clean working tree with only the intentional mods that aren't already upstream.

Conflict resolution pattern (Jul 2026) — for light drift

When git stash apply succeeds with only a few conflicts:

  1. git stash apply 'stash@{N}' (not pop).
  2. For each UU file:
    • Upstream already has equivalent → take upstream.
    • Stash has unique behavior on moved upstream code → merge (e.g. keep _final_delivery_adapter + add and not _voice_only).
    • Stash hunk landed in wrong method after drift → discard stash side; confirm real wire elsewhere.
  3. Clear all conflict markers.
  4. python3 -m py_compile on every touched .py.
  5. git restore --staged . so changes are plain local mods.
  6. Only then git stash drop 'stash@{N}'.
  7. Gateway/CLI restart for gateway/voice/SOUL runtime paths.

Intentional local mods on this install (post 2026-07-25)

  • docker/SOUL.md — custom Primary Directive SOUL
  • cli.py, hermes_cli/voice.py, tools/tts_tool.py — TTS 600s + Kokoro response.content
  • tools/transcription_tools.py.oga
  • hermes_cli/cli_agent_setup_mixin.py — model alias routing
  • gateway/platforms/base.py — voice_only text suppression merged with current delivery path

Not re-applied: hang-fix class (upstream daemon_pool), obsolete April SearXNG/delegate rewrite, broken load_soul_identity one-liner.

Cross-skill: voice patches also under voice-systems.

Pitfalls

  • Confusing stashed source edits with missing config/skills after update.
  • Dropping stashes by old index numbers after a prior drop (renumbering).
  • Using whole-tree git diff main stash@{N} to decide obsolescence — false positives.
  • Re-applying ancient stashes blind — inspect + selective restore only with user direction.
  • Re-applying hang-fix / SearXNG stashes that regress newer upstream implementations.
  • Leaving merge conflict markers or staged half-applies; always py_compile + unstage.
  • Forgetting gateway restart after applying gateway/voice/SOUL source patches.
  • Any running gateway process holds stale Python modules in sys.modules after hermes update touches source on disk — not just for stash re-applies. Symptom: cron jobs (or any code path) that were working fine suddenly throw ImportError: cannot import name 'X' from 'module' even though the symbol clearly exists when you read the file. This isn't specific to manual patch re-application — a plain hermes update while a gateway is running for that profile can trigger it too. Fix: hermes -p <profile> gateway restart for every profile with a live gateway after any source update, not just the one you're actively debugging. Verify via stat -c '%y' <file> vs systemctl --user show <service> -p ExecMainStartTimestamp (or ps -o lstart -p <pid>) — if the file mtime is after the process start time, the running process is stale. See hermes-cron-management skill's stale-gateway-module-cache pitfall for the full diagnostic recipe and a worked cron-job example.
  • Bundled hermes-agent skill is protected; put install/update ops learnings here, not there.
  • Empty stashes (0 lines). When hermes update runs and there are no local changes to stash, it may still create an empty autostash. git stash show --stat shows nothing. These are pure noise — drop immediately.

Quick user-facing explanation

Hermes is a git checkout under ~/.hermes/hermes-agent. Update stashes uncommitted source edits so pull can succeed. Your profiles/config are separate. Stashes named hermes-update-autostash-* are those parked edits.