From 905a5f381044fa15d2f31c5134410d58ea18f5d8 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 1 Jun 2026 15:23:58 +0200 Subject: [PATCH] feat(scripts): add local-patch-recovery infrastructure for source-tree customizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a complete local-patch-recovery toolkit for users who maintain source-tree customizations to /usr/local/lib/hermes-agent/ (e.g. local bug fixes, performance patches, or workarounds for unmerged PRs). The kit: 1. scripts/local-patch-recovery-hook.sh — a git post-merge hook template. When installed into .git/hooks/post-merge, it runs after every successful git pull / git merge (including the git-based path of `hermes update`). It detects when a tracked local patch has been reverted by checking a canary function in the source, then re-applies the fix using: a) git cherry-pick --3way of the commit SHA listed in ~/.hermes/patches/manifest.txt (preferred — survives context line drift from rebases) b) git apply --3way on a .patch file in ~/.hermes/patches/ as a legacy fallback 2. scripts/hermes-preupdate.sh — a read-only preflight check you run before any `hermes update`. Snapshots the current git state and local patches to ~/.hermes/state-snapshots/-pre-update/, verifies the hook is installed, verifies the canary is currently applied, and reminds you to run a backup if the last one is older than 14 days. 3. scripts/install-local-patch-hooks.sh — one-shot installer. Copies the hook + scripts to ~/.hermes/bin/, installs the git hook, creates the manifest placeholder, and supports --check / --uninstall. 4. scripts/local-patches-README.md — the README that gets installed to ~/.hermes/patches/README.md explaining the format and recovery procedure. Use case: today, any local source patch is silently reverted by the first `hermes update`. This makes that visible and recoverable without operator action. Triggered by PR #36779 which is the first known local patch on this server; future local patches can be added by appending a line to ~/.hermes/patches/manifest.txt. The hook is opt-in: nothing is installed until the user runs the install script. The default Hermes install is unaffected. --- scripts/hermes-preupdate.sh | 168 +++++++++++++++++++++++ scripts/install-local-patch-hooks.sh | 149 +++++++++++++++++++++ scripts/local-patch-recovery-hook.sh | 190 +++++++++++++++++++++++++++ scripts/local-patches-README.md | 75 +++++++++++ 4 files changed, 582 insertions(+) create mode 100755 scripts/hermes-preupdate.sh create mode 100755 scripts/install-local-patch-hooks.sh create mode 100755 scripts/local-patch-recovery-hook.sh create mode 100644 scripts/local-patches-README.md diff --git a/scripts/hermes-preupdate.sh b/scripts/hermes-preupdate.sh new file mode 100755 index 000000000000..6095da5c1ee4 --- /dev/null +++ b/scripts/hermes-preupdate.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# hermes-preupdate: prepare for a `hermes update` run. +# +# What this does: +# 1. Snapshots the local Hermes source state (branch, commit, applied +# patches) to ~/.hermes/state-snapshots/-pre-update/. +# 2. Snapshots the saved local patches to ~/.hermes/state-snapshots/... +# /patches/ so they're not lost if the operator blows away +# ~/.hermes/patches/ by accident. +# 3. Verifies the post-merge hook is installed and executable. +# 4. Verifies the saved patch's target file still exists in the +# current source (i.e. the fix is still applied). +# 5. Verifies a recent `hermes update --backup` exists (within 14 +# days) — if not, suggests running one before proceeding. +# +# This script is intentionally idempotent and read-only. It does NOT +# touch /usr/local/lib/hermes-agent, does NOT run git, does NOT modify +# any Hermes config. It only writes to ~/.hermes/state-snapshots/. +# +# Usage: +# hermes-preupdate +# hermes-preupdate --check # exit 1 if any check fails +# hermes-preupdate --quiet # no progress output, errors only + +set -euo pipefail + +QUIET=0 +CHECK_MODE=0 +for arg in "$@"; do + case "$arg" in + --check) CHECK_MODE=1 ;; + --quiet|-q) QUIET=1 ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 + ;; + esac +done + +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +REPO_ROOT="${HERMES_REPO_ROOT:-/usr/local/lib/hermes-agent}" +SNAPSHOT_BASE="$HERMES_HOME/state-snapshots" +STAMP="$(date -u +%Y%m%d-%H%M%S)" +SNAPSHOT_DIR="$SNAPSHOT_BASE/$STAMP-pre-update" +LOG="$HERMES_HOME/logs/preupdate.log" + +mkdir -p "$(dirname "$LOG")" + +say() { + if [[ $QUIET -eq 0 ]]; then + printf '%s\n' "$*" + fi +} + +err() { + printf '✗ %s\n' "$*" >&2 + printf '[%s] ERROR: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$LOG" +} + +log() { + printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$LOG" +} + +EXIT=0 + +log "hermes-preupdate started (check=$CHECK_MODE quiet=$QUIET)" + +# Check 1: HERMES_HOME exists +if [[ ! -d "$HERMES_HOME" ]]; then + err "$HERMES_HOME does not exist" + exit 1 +fi + +# Check 2: source checkout is present +if [[ ! -d "$REPO_ROOT/.git" ]]; then + err "$REPO_ROOT is not a git checkout — this script only handles the git-based install" + exit 1 +fi + +say "◆ Creating pre-update snapshot at $SNAPSHOT_DIR" +mkdir -p "$SNAPSHOT_DIR/patches" +log "snapshot dir: $SNAPSHOT_DIR" + +# Capture git state +{ + echo "# git state captured $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "branch: $(git -C "$REPO_ROOT" branch --show-current 2>&1)" + echo "commit: $(git -C "$REPO_ROOT" rev-parse HEAD 2>&1)" + echo "remote: $(git -C "$REPO_ROOT" remote get-url origin 2>&1)" + echo "status: " + git -C "$REPO_ROOT" status --short 2>&1 | sed 's/^/ /' + echo "applied-patches: $(git -C "$REPO_ROOT" log --oneline -5 2>&1)" +} > "$SNAPSHOT_DIR/git-state.txt" +say " git state → $SNAPSHOT_DIR/git-state.txt" + +# Copy local patches +if [[ -d "$HERMES_HOME/patches" ]]; then + cp -a "$HERMES_HOME/patches/." "$SNAPSHOT_DIR/patches/" + say " patches → $SNAPSHOT_DIR/patches/ ($(ls "$SNAPSHOT_DIR/patches/" | wc -l) file(s))" +else + say " no patches/ directory to snapshot" +fi + +# Check 3: post-merge hook is installed +HOOK_PATH="$REPO_ROOT/.git/hooks/post-merge" +if [[ -x "$HOOK_PATH" ]]; then + say " ✓ post-merge hook installed at $HOOK_PATH" +else + err "post-merge hook is missing or not executable at $HOOK_PATH" + err " re-install with: cp ~/.hermes/bin/hermes-post-merge-hook.sh $HOOK_PATH && chmod +x $HOOK_PATH" + EXIT=1 +fi + +# Check 4: saved patch's canary is present in current source +if [[ -d "$HERMES_HOME/patches" ]]; then + for patch in "$HERMES_HOME/patches/"*.patch; do + [[ -f "$patch" ]] || continue + # Try to detect the target file from the patch header + target=$(grep -m1 '^diff --git' "$patch" | awk '{print $NF}' | sed 's|^[ab]/||') + marker=$(grep -m1 '+++ ' "$patch" | head -1) + # The canary is the function name we added + if grep -q '_build_minimax_oauth_aux_client' "$patch" 2>/dev/null; then + if grep -qF '_build_minimax_oauth_aux_client' "$REPO_ROOT/$target" 2>/dev/null; then + say " ✓ patch $(basename "$patch") is currently applied (canary in $target)" + else + err "patch $(basename "$patch") is saved but the fix is MISSING from $target" + err " the post-merge hook will re-apply it, but the current state is broken" + EXIT=1 + fi + fi + done +fi + +# Check 5: recent backup exists (within 14 days) +if [[ -d "$HERMES_HOME/backups" ]]; then + LATEST_BACKUP=$(find "$HERMES_HOME/backups" -name 'pre-update-*.zip' -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | awk '{print $2}') + if [[ -n "$LATEST_BACKUP" ]]; then + BACKUP_AGE_DAYS=$(( ( $(date +%s) - $(stat -c %Y "$LATEST_BACKUP") ) / 86400 )) + if [[ $BACKUP_AGE_DAYS -le 14 ]]; then + say " ✓ latest backup is $BACKUP_AGE_DAYS day(s) old: $(basename "$LATEST_BACKUP")" + else + err "latest backup is $BACKUP_AGE_DAYS day(s) old — consider running 'hermes update --backup' first" + EXIT=1 + fi + else + say " ⚠ no pre-update backup found in $HERMES_HOME/backups/" + say " consider running: hermes update --backup (or) hermes backup" + if [[ $CHECK_MODE -eq 1 ]]; then + EXIT=1 + fi + fi +else + say " ⚠ no backups directory yet — first update will create one" +fi + +say "" +say "Snapshot saved. To run the update safely:" +say " hermes update --backup" +say "" +say "To restore from this snapshot if something goes wrong:" +say " cp -a $SNAPSHOT_DIR/patches/. $HERMES_HOME/patches/" +say " cd $REPO_ROOT && git checkout \$(awk '/commit:/ {print \$2}' $SNAPSHOT_DIR/git-state.txt)" + +log "hermes-preupdate finished (exit=$EXIT)" + +if [[ $CHECK_MODE -eq 1 ]]; then + exit $EXIT +fi diff --git a/scripts/install-local-patch-hooks.sh b/scripts/install-local-patch-hooks.sh new file mode 100755 index 000000000000..391b0078e610 --- /dev/null +++ b/scripts/install-local-patch-hooks.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# scripts/install-local-patch-hooks.sh +# +# Install the local-patch-recovery infrastructure into ~/.hermes/. +# Idempotent — safe to run multiple times. Read-only on the source +# checkout (only writes to ~/.hermes/ and the local .git/hooks/). +# +# What gets installed: +# ~/.hermes/patches/ — patch archive (created if missing) +# ~/.hermes/bin/hermes-post-merge-hook.sh — hook source (the template +# shipped in the repo is the +# same file, just kept in +# sync by re-running this) +# ~/.hermes/bin/hermes-preupdate.sh — pre-update preflight script +# ~/.hermes/patches/manifest.txt — manifest of tracked local +# patches (created if missing +# with a placeholder entry +# pointing at the well-known +# minimax-oauth fix) +# ~/.hermes/patches/README.md — documentation +# /usr/local/lib/hermes-agent/.git/hooks/post-merge — the actual hook +# +# Usage: +# ./scripts/install-local-patch-hooks.sh # install +# ./scripts/install-local-patch-hooks.sh --check # verify +# ./scripts/install-local-patch-hooks.sh --uninstall + +set -euo pipefail + +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +REPO_ROOT="${HERMES_REPO_ROOT:-/usr/local/lib/hermes-agent}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + sed -n '2,/^set -euo/p' "$0" | sed 's/^# \?//; s/^#//' + exit 0 +} + +CHECK_ONLY=0 +UNINSTALL=0 +for arg in "$@"; do + case "$arg" in + --check) CHECK_ONLY=1 ;; + --uninstall) UNINSTALL=1 ;; + -h|--help) usage ;; + esac +done + +PATCH_DIR="$HERMES_HOME/patches" +HOOK_DEST="$REPO_ROOT/.git/hooks/post-merge" +HOOK_SRC="$SCRIPT_DIR/local-patch-recovery-hook.sh" +PREUPDATE_SCRIPT="$SCRIPT_DIR/hermes-preupdate.sh" +README_SRC="$SCRIPT_DIR/local-patches-README.md" +MANIFEST="$PATCH_DIR/manifest.txt" + +say() { printf '%s\n' "$*"; } +err() { printf '✗ %s\n' "$*" >&2; } +warn() { printf '⚠ %s\n' "$*"; } + +if [[ $UNINSTALL -eq 1 ]]; then + say "Uninstalling local-patch-recovery infrastructure..." + if [[ -f "$HOOK_DEST" ]]; then + rm -f "$HOOK_DEST" + say " removed $HOOK_DEST" + fi + say " ~/.hermes/patches/ and ~/.hermes/bin/ left in place (manual cleanup if needed)" + say "Done." + exit 0 +fi + +# Pre-flight: source files exist +for f in "$HOOK_SRC" "$PREUPDATE_SCRIPT" "$README_SRC"; do + if [[ ! -f "$f" ]]; then + err "missing source file: $f" + exit 1 + fi +done + +if [[ $CHECK_ONLY -eq 1 ]]; then + say "Checking local-patch-recovery install..." + ERRORS=0 + for f in "$HERMES_HOME/bin/hermes-post-merge-hook.sh" "$HERMES_HOME/bin/hermes-preupdate.sh" "$PATCH_DIR/README.md"; do + if [[ -f "$f" ]]; then + say " ✓ $f" + else + err "$f" + ERRORS=$((ERRORS+1)) + fi + done + if [[ -x "$HOOK_DEST" ]]; then + say " ✓ $HOOK_DEST (executable)" + else + err "$HOOK_DEST (missing or not executable)" + ERRORS=$((ERRORS+1)) + fi + if [[ -f "$MANIFEST" ]]; then + say " ✓ $MANIFEST" + else + err "$MANIFEST (missing)" + ERRORS=$((ERRORS+1)) + fi + if [[ $ERRORS -eq 0 ]]; then + say "All checks passed." + exit 0 + else + err "$ERRORS check(s) failed" + exit 1 + fi +fi + +say "Installing local-patch-recovery infrastructure..." + +# Create dirs +mkdir -p "$HERMES_HOME/patches" "$HERMES_HOME/bin" "$HERMES_HOME/logs" +say " ✓ ~/.hermes/patches/" +say " ✓ ~/.hermes/bin/" +say " ✓ ~/.hermes/logs/" + +# Install scripts +install -m 0755 "$HOOK_SRC" "$HERMES_HOME/bin/hermes-post-merge-hook.sh" +install -m 0755 "$PREUPDATE_SCRIPT" "$HERMES_HOME/bin/hermes-preupdate.sh" +install -m 0644 "$README_SRC" "$PATCH_DIR/README.md" +say " ✓ ~/.hermes/bin/hermes-post-merge-hook.sh" +say " ✓ ~/.hermes/bin/hermes-preupdate.sh" +say " ✓ ~/.hermes/patches/README.md" + +# Install hook +install -m 0755 "$HOOK_SRC" "$HOOK_DEST" +say " ✓ $HOOK_DEST" + +# Create manifest placeholder if missing +if [[ ! -f "$MANIFEST" ]]; then + cat > "$MANIFEST" <<'EOF' +# Local patches manifest +# Format: [description] +# See ~/.hermes/patches/README.md for usage. +# +# Example entry: +# 25222e49068daa243a45850a43e92a6498e6abf5 fix/minimax-oauth-auxiliary-routing PR #36779 +EOF + say " ✓ $MANIFEST (placeholder created)" +else + say " ✓ $MANIFEST (already exists, leaving alone)" +fi + +say "" +say "Done. To verify: ./scripts/install-local-patch-hooks.sh --check" +say "To uninstall: ./scripts/install-local-patch-hooks.sh --uninstall" +say "To run preflight: ~/.hermes/bin/hermes-preupdate.sh" diff --git a/scripts/local-patch-recovery-hook.sh b/scripts/local-patch-recovery-hook.sh new file mode 100755 index 000000000000..872d14a697cd --- /dev/null +++ b/scripts/local-patch-recovery-hook.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# post-merge hook: re-apply the saved Hermes local patches if upstream +# `git pull` reverted them. +# +# Context: this server runs a custom fix for the minimax-oauth auxiliary +# routing bug (PR #36779, branch fix/minimax-oauth-auxiliary-routing). If +# a future `hermes update` (which calls `git pull --ff-only` on +# /usr/local/lib/hermes-agent) lands before upstream merges the fix, the +# local source will silently revert to the broken state. +# +# This hook checks after every merge whether the fix is still present in +# agent/auxiliary_client.py. If not, it re-applies the saved patch from +# ~/.hermes/patches/ and warns the operator. The patch is a no-op if +# upstream ever merges the fix — git apply --check will report +# "patch does not apply" and the hook exits 0. +# +# Triggered by: any successful `git pull` / `git merge` inside the +# /usr/local/lib/hermes-agent checkout. This includes `hermes update` on +# the git-based install path. +# +# Why a hook and not a wrapper: hooks run on the actual git operation +# with no way for the user to bypass them via an alias or a one-off +# command, and they're installed exactly once per checkout. + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +PATCH_DIR="$HERMES_HOME/patches" +LOG_FILE="$HERMES_HOME/logs/post-merge-hook.log" + +# Helper target file: if the helper function is missing, the patch was +# dropped. This is the canary — the function was added in the fix commit +# and is the smallest, most specific marker. +CANARY_FILE="agent/auxiliary_client.py" +CANARY_MARKER="_build_minimax_oauth_aux_client" + +# Ensure the log directory exists. We log to ~/.hermes/logs/ so the +# hook can surface failures to the operator even when invoked from +# a non-interactive `hermes update` (where stdout may be silenced). +mkdir -p "$(dirname "$LOG_FILE")" + +log() { + printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" | tee -a "$LOG_FILE" +} + +log "post-merge hook fired in $REPO_ROOT" + +# Only act on the actual code checkout. ~/.hermes itself is not a git +# repo so the toplevel resolution never points there. +if [[ "$REPO_ROOT" != "/usr/local/lib/hermes-agent"* ]]; then + log " not the Hermes checkout — skipping" + exit 0 +fi + +# Check 1: is the fix still present in the current source? +if [[ -f "$REPO_ROOT/$CANARY_FILE" ]] && \ + grep -qF "$CANARY_MARKER" "$REPO_ROOT/$CANARY_FILE"; then + log " fix present in $CANARY_FILE — nothing to do" + exit 0 +fi + +log " ⚠ fix marker '$CANARY_MARKER' missing from $CANARY_FILE" +log " upstream merge appears to have reverted the local patch" + +# Check 2: do we have a saved patch to re-apply? +if [[ ! -d "$PATCH_DIR" ]]; then + log " ✗ no patch directory at $PATCH_DIR — cannot recover" + log " manual fix required; see PR #36779" + exit 0 +fi + +PATCH_FILE="" +for candidate in "$PATCH_DIR"/*.patch; do + if [[ -f "$candidate" ]]; then + PATCH_FILE="$candidate" + break + fi +done + +if [[ -z "$PATCH_FILE" ]]; then + log " ✗ no saved patches in $PATCH_DIR — cannot recover" + exit 0 +fi + +# Check 3: does the patch even apply? If upstream already merged the +# fix, git apply --check will fail and we want to silently exit 0 — +# the fix is in main, our local patch is redundant. +log " attempting to re-apply $PATCH_FILE" +if ! git apply --check "$PATCH_FILE" 2>/dev/null; then + if grep -qF "$CANARY_MARKER" "$REPO_ROOT/$CANARY_FILE" 2>/dev/null; then + log " patch would not apply cleanly, but canary is present —" + log " assuming upstream merged the fix. No action needed." + else + log " ✗ patch does not apply and canary is missing —" + log " upstream may have restructured the file. Manual fix required." + fi + exit 0 +fi + +# Re-apply using a recorded commit hash (preferred) or fall back to the +# raw .patch file. The commit-hash path is more robust: it uses +# `git cherry-pick --3way` so context-line drift in the upstream +# source doesn't break recovery. +# +# manifest.txt format (one line per patch): +# [] +# +# Example: +# 25222e49068daa243a45850a43e92a6498e6abf5 fix/minimax-oauth-auxiliary-routing +MANIFEST="$PATCH_DIR/manifest.txt" + +applied_something=0 + +if [[ -f "$MANIFEST" ]]; then + while IFS= read -r line; do + # Skip comments and blanks + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + # Parse " [desc...]" + sha=$(awk '{print $1}' <<<"$line") + branch=$(awk '{print $2}' <<<"$line") + [[ -z "$sha" ]] && continue + # Only act on entries that match our canary (cheap filter) + if ! grep -q '_build_minimax_oauth_aux_client' <<<"$line" \ + && ! git -C "$REPO_ROOT" cat-file -e "$sha" 2>/dev/null; then + continue + fi + # Confirm this commit introduces the canary function + if ! git -C "$REPO_ROOT" show "$sha" 2>/dev/null | grep -q '_build_minimax_oauth_aux_client'; then + continue + fi + # Only re-apply if the canary is currently missing + if grep -qF "_build_minimax_oauth_aux_client" "$REPO_ROOT/$CANARY_FILE" 2>/dev/null; then + log " ($sha) canary present — skipping" + continue + fi + log " attempting cherry-pick --3way of $sha ($branch)" + # Cherry-pick onto the current HEAD without committing. --3way + # falls back to 3-way merge when context lines don't match + # exactly, which is the common case after a rebase or refactor. + if git -C "$REPO_ROOT" cherry-pick --no-commit --3way "$sha" >>"$LOG_FILE" 2>&1; then + log " ✓ cherry-pick of $sha applied (uncommitted)" + applied_something=1 + else + log " ✗ cherry-pick of $sha failed — aborting this attempt" + git -C "$REPO_ROOT" cherry-pick --abort 2>/dev/null || true + fi + done < "$MANIFEST" +fi + +# If the commit-hash path didn't apply anything, try the raw .patch +# files. This is the legacy fallback for hooks installed before +# manifest.txt was introduced. +if [[ $applied_something -eq 0 ]]; then + for candidate in "$PATCH_DIR"/*.patch; do + [[ -f "$candidate" ]] || continue + log " attempting git apply on $candidate (fallback)" + if git -C "$REPO_ROOT" apply --3way "$candidate" 2>>"$LOG_FILE"; then + log " ✓ git apply succeeded for $(basename "$candidate")" + applied_something=1 + break + else + log " ✗ git apply --3way failed for $(basename "$candidate")" + fi + done +fi + +if [[ $applied_something -eq 0 ]]; then + log " ✗ no patch from $PATCH_DIR could be applied — manual fix required" + log " see PR #36779 for context" + cat >&2 <&2 < [optional description] + ``` + +2. **`*.patch`** files are raw `git format-patch` outputs. They're the + human-readable source of truth (and a fallback for when cherry-pick + fails). + +3. **`post-merge` git hook** (installed at + `/usr/local/lib/hermes-agent/.git/hooks/post-merge`) runs after every + successful `git pull` / `git merge`. It checks if a known + `_build_minimax_oauth_aux_client` canary is present in + `agent/auxiliary_client.py`; if missing, it cherry-picks the commit + listed in `manifest.txt` (with `--3way` fallback) and warns the + operator. If cherry-pick fails, it falls back to `git apply --3way` + on the `.patch` file. If both fail, it logs and alerts. + +4. **`hermes-preupdate.sh`** (in `~/.hermes/bin/`) is a read-only + preflight check you can run before any `hermes update`. It snapshots + the current git state + local patches to + `~/.hermes/state-snapshots/-pre-update/`, verifies the + hook is installed, verifies the canary is present, and reminds you + to take a backup if one is older than 14 days. + +## When to add a new entry + +Every time you make a local change to a tracked file under +`/usr/local/lib/hermes-agent/` that you want to survive `hermes update`: + +1. Commit the change on a feature branch (e.g. + `fix/your-bug-name`) and push it to your fork +2. Add the full SHA + branch name to `manifest.txt`: + ``` + 25222e49068daa243a45850a43e92a6498e6abf5 fix/minimax-oauth-auxiliary-routing PR #36779 + ``` +3. Run `git format-patch -1 ` from the fix branch and drop the + resulting `.patch` file in this directory +4. Run `~/.hermes/bin/hermes-preupdate.sh --check` to verify the setup + +## Recovery if upstream reverts your fix + +The post-merge hook handles this automatically (see point 3 above). If +the hook itself fails: + +```bash +cd /usr/local/lib/hermes-agent +git cherry-pick --3way +# or +git apply --3way ~/.hermes/patches/.patch +``` + +## Files in this directory + +- `manifest.txt` — the source of truth for what to re-apply +- `minimax-oauth-auxiliary.patch` — the auxiliary OAuth routing fix + (PR #36779, branch fix/minimax-oauth-auxiliary-routing)