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

236 lines
11 KiB
Markdown

---
name: hermes-source-updates
description: "Use when hermes update stashes source or cleanup is needed."
version: 1.1.0
author: Hermes Agent
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [hermes, update, git, stash, install, source]
related_skills: [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)
```bash
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:
```bash
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:
```bash
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.
```bash
# 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.py``DaemonThreadPoolExecutor` |
| `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 |
```bash
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:
```bash
# 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.