feat(update): stash/restore by default + settable discard for non-interactive updates (reverts #38542, #39568) (#39645)

* Revert "fix(update): require managed marker before destructive clean"

This reverts commit c8e80cd0bf.

* Revert "fix(update): stop stash/restore from clobbering desktop source on managed clones (#38542)"

This reverts commit 8a19884bf3.

* chore(install): keep npm ci desktop-build fix after stash revert

The destructive-clean reverts (#38542/#39568) pulled the desktop
workspace install back to bare `npm install`. The npm ci -> npm install
fallback is orthogonal build-correctness (avoids the Windows
workspace-hoisting flake where install reports up-to-date against a
stale marker while node_modules is empty, breaking tsc -b). Preserve it.

* feat(update): settable stash-or-discard for non-interactive local changes

Adds updates.non_interactive_local_changes (stash | discard, default
stash). Governs ONLY non-interactive updates (desktop/chat app, gateway,
--yes) — interactive terminal updates always stash-and-ask, unchanged.

- config.py: new key under existing updates section; _config_version 26->27.
- main.py: _cmd_update_impl detects non-interactive (gateway/--yes/no-TTY),
  reads the setting; new _discard_stashed_changes() drops the stash
  (stash-and-drop, never reset --hard/clean -fd, so ignored paths survive).
  Post-pull restore site branches on it; the bail-out and up-to-date
  restores always preserve work.
- web_server.py + apps/desktop settings: exposes it as a stash/discard
  select (Advanced section, In-App Update Local Changes).
- docs + tests (discard drops, stash restores, interactive ignores setting,
  missing section defaults to stash).

* fix(install.ps1): stash/restore instead of reset --hard on Windows update

The PR reverted the destructive update path to stash/restore everywhere
except scripts/install.ps1, whose managed-clone update path still ran
`git reset --hard HEAD` before checkout — silently destroying agent-edited
tracked source on Windows (the same #38542 data-loss class the PR fixes).

- Replace `git reset --hard HEAD` with stash-before-checkout +
  restore-after-checkout, mirroring install.sh. Untracked files are
  included so agent-created dirs (e.g. tinker-atropos/) survive.
- Keep `core.autocrlf false` (it prevents the phantom CRLF dirt that made
  the stash necessary; it's also load-bearing for a clean restore).
- Wrap all three checkout modes (Commit/Tag/Branch); Branch case now uses
  `git pull --ff-only` so local commits are never clobbered.
- Only prompt to restore when a real console is attached (UserInteractive
  + non-redirected stdin/stdout + ConsoleHost); the desktop Update button
  and bootstrap have no usable console, so they default to restore and
  never hang on Read-Host.
- On restore conflict or a failed update, the stash is preserved with
  recovery instructions — work is never silently dropped.

Validated on Windows (PowerShell 5.1, git 2.54): AST parse clean;
E2E non-conflicting restore applies+drops cleanly with ignored paths
(node_modules) untouched; conflicting restore preserves the stash.

---------

Co-authored-by: alt-glitch <balyan.sid@gmail.com>
This commit is contained in:
Teknium 2026-06-05 05:00:10 -07:00 committed by GitHub
parent 947e21b3d6
commit 72eb42d9ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 335 additions and 255 deletions

View file

@ -1063,6 +1063,7 @@ function Install-Repository {
# EAP=Stop. We rely on $LASTEXITCODE for actual failures.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$autostashRef = ""
try {
# This is a MANAGED checkout, not a repo the user edits. Git for
# Windows defaults to core.autocrlf=true, which renormalizes the
@ -1071,12 +1072,23 @@ function Install-Repository {
# show as locally modified even though nobody touched them. A
# bare `git checkout` then aborts with "Your local changes would
# be overwritten by checkout", which is exactly the failure GUI
# users hit on update. Two-part fix: (1) stop creating the dirt
# by pinning autocrlf=false on this clone, (2) discard any
# pre-existing dirt with a hard reset before the checkout. Safe
# because nothing here is user-authored.
# users hit on update. Pin autocrlf=false so the dirt is never
# created in the first place.
git -c windows.appendAtomically=false config core.autocrlf false 2>$null
git -c windows.appendAtomically=false reset --hard HEAD 2>$null
# Preserve any real local changes before the checkout instead of
# discarding them with `reset --hard HEAD`. The old hard reset
# silently destroyed agent-edited source on managed clones (the
# #38542 data-loss class). Stash + restore mirrors install.sh:
# nothing is lost, and a failed restore leaves the work in a
# git stash for manual recovery. Untracked files are included so
# agent-created dirs (e.g. tinker-atropos/) survive too.
$statusOut = git -c windows.appendAtomically=false status --porcelain 2>$null
if (-not [string]::IsNullOrWhiteSpace(($statusOut -join "`n"))) {
$stashName = "hermes-install-autostash-" + (Get-Date -Format "yyyyMMdd-HHmmss")
Write-Info "Local changes detected, stashing before update..."
git -c windows.appendAtomically=false stash push --include-untracked -m "$stashName"
if ($LASTEXITCODE -eq 0) { $autostashRef = "stash@{0}" }
}
git -c windows.appendAtomically=false fetch origin
if ($LASTEXITCODE -ne 0) { throw "git fetch failed (exit $LASTEXITCODE)" }
# Precedence: Commit > Tag > Branch. Commit and Tag check
@ -1095,10 +1107,62 @@ function Install-Repository {
} else {
git -c windows.appendAtomically=false checkout $Branch
if ($LASTEXITCODE -ne 0) { throw "git checkout $Branch failed (exit $LASTEXITCODE)" }
git -c windows.appendAtomically=false pull origin $Branch
git -c windows.appendAtomically=false pull --ff-only origin $Branch
if ($LASTEXITCODE -ne 0) { throw "git pull failed (exit $LASTEXITCODE)" }
}
if ($autostashRef) {
# Default to restoring so work is never silently dropped.
# Only prompt when we're certain a human can answer: an
# interactive session AND a real, non-redirected console on
# both stdin and stdout. The desktop "Update" button and
# bootstrap run the installer without a usable console -- in
# those cases Read-Host would hang or return empty, so we
# skip the prompt and just restore (the safe default).
$restoreNow = $true
$hasConsole = $false
try {
$hasConsole = (
[Environment]::UserInteractive `
-and (-not [Console]::IsInputRedirected) `
-and (-not [Console]::IsOutputRedirected) `
-and ($Host.Name -eq "ConsoleHost")
)
} catch { $hasConsole = $false }
if ($hasConsole) {
Write-Warn "Local changes were stashed before updating."
Write-Warn "Restoring them may reapply local customizations onto the updated codebase."
$restoreAnswer = Read-Host "Restore local changes now? [Y/n]"
if ($restoreAnswer -match '^(n|no)$') { $restoreNow = $false }
}
if ($restoreNow) {
Write-Info "Restoring local changes..."
git -c windows.appendAtomically=false stash apply $autostashRef
if ($LASTEXITCODE -eq 0) {
git -c windows.appendAtomically=false stash drop $autostashRef 2>$null
Write-Warn "Local changes were restored on top of the updated codebase."
Write-Warn "Review git diff / git status if Hermes behaves unexpectedly."
} else {
Write-Err "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash."
Write-Info "Resolve manually with: git stash apply $autostashRef"
throw "git stash apply failed after update"
}
} else {
Write-Info "Skipped restoring local changes."
Write-Info "Your changes are still preserved in git stash."
Write-Info "Restore manually with: git stash apply $autostashRef"
}
$autostashRef = ""
}
} finally {
if ($autostashRef) {
# We stashed but never reached the restore block (a fetch/
# checkout/pull failure threw). Leave the stash in place and
# tell the user how to recover it -- never silently drop it.
Write-Warn "Update did not complete. Your local changes are preserved in git stash."
Write-Info "Restore manually with: git stash apply $autostashRef"
}
$ErrorActionPreference = $prevEAP
Pop-Location
}

View file

@ -1097,24 +1097,50 @@ clone_repo() {
log_info "Existing installation found, updating..."
cd "$INSTALL_DIR"
# This is a managed clone the user never edits, so any working-tree
# dirt is git artifact (CRLF renormalization, npm lockfile churn,
# files left behind when a directory was deleted upstream such as
# apps/bootstrap-installer/). The old path stashed that dirt and
# re-applied it after the pull, but the stash/restore cycle has
# clobbered freshly-pulled source files (apps/desktop/ →
# "[UNRESOLVED_ENTRY] Cannot resolve entry module index.html").
# Discard the dirt with a hard reset instead — mirrors install.ps1's
# update path. Fork users customize via `hermes update`, which keeps
# the stash machinery; the installer is a managed-only entry point.
git fetch origin
local autostash_ref=""
if [ -n "$(git status --porcelain)" ]; then
log_info "Discarding working-tree changes on managed clone before update..."
git reset --hard HEAD >/dev/null 2>&1 || true
git clean -fd >/dev/null 2>&1 || true
local stash_name
stash_name="hermes-install-autostash-$(date -u +%Y%m%d-%H%M%S)"
log_info "Local changes detected, stashing before update..."
git stash push --include-untracked -m "$stash_name"
autostash_ref="stash@{0}"
fi
git fetch origin
git checkout "$BRANCH"
git reset --hard "origin/$BRANCH"
git pull --ff-only origin "$BRANCH"
if [ -n "$autostash_ref" ]; then
local restore_now="yes"
if [ -t 0 ] && [ -t 1 ]; then
echo
log_warn "Local changes were stashed before updating."
log_warn "Restoring them may reapply local customizations onto the updated codebase."
printf "Restore local changes now? [Y/n] "
read -r restore_answer
case "$restore_answer" in
""|y|Y|yes|YES|Yes) restore_now="yes" ;;
*) restore_now="no" ;;
esac
fi
if [ "$restore_now" = "yes" ]; then
log_info "Restoring local changes..."
if git stash apply "$autostash_ref"; then
git stash drop "$autostash_ref" >/dev/null
log_warn "Local changes were restored on top of the updated codebase."
log_warn "Review git diff / git status if Hermes behaves unexpectedly."
else
log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash."
log_info "Resolve manually with: git stash apply $autostash_ref"
exit 1
fi
else
log_info "Skipped restoring local changes."
log_info "Your changes are still preserved in git stash."
log_info "Restore manually with: git stash apply $autostash_ref"
fi
fi
else
log_error "Directory exists but is not a git repository: $INSTALL_DIR"
log_info "Remove it or choose a different directory with --dir"