Skip to content

fix(cli): restore visible git fetch progress and defer autostash until update confirmed (#3523) - #62962

Open
sahilthakur456111-stack wants to merge 1 commit into
NousResearch:mainfrom
sahilthakur456111-stack:fix/3523-update-stash-and-fetch
Open

fix(cli): restore visible git fetch progress and defer autostash until update confirmed (#3523)#62962
sahilthakur456111-stack wants to merge 1 commit into
NousResearch:mainfrom
sahilthakur456111-stack:fix/3523-update-stash-and-fetch

Conversation

@sahilthakur456111-stack

Copy link
Copy Markdown

Fixes #3523

Symptom

Regression from #3492 introduced two user-visible issues in hermes update:

  1. Silent git fetchgit fetch ran with capture_output=True, swallowing git's progress output (object enumeration, remote branch listing). Users saw only the literal string "→ Fetching updates..." with no indication anything was happening.

  2. Autostash on every rungit stash push ran BEFORE we knew whether there was an update to apply. On an up-to-date tree with local changes, every hermes update call would:

    • Create a stash named hermes-update-autostash-<timestamp>
    • Trigger an interactive restore prompt ("Restore local changes now? [Y/n]")
    • Restore the stash
    • Exit cleanly

    …for nothing. Clutters the stash list, breaks --yes cron updates, and prompts in non-interactive contexts.

Repro

# On up-to-date main with one local uncommitted change
echo "# local note" >> README.md
hermes update

Expected: update completes silently (or with progress), no stash prompt.
Actual: "⚠ Local changes were stashed before updating." → "Restore local changes now? [Y/n]" prompt → stash restored.

Root Cause

# BEFORE — hermes_cli/main.py:9620 (and 9654)
if current_branch != branch:
    auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)  # ← unconditional
    checkout_result = ...
else:
    auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)  # ← unconditional

# THEN we check for updates...
result = subprocess.run(git_cmd + ["rev-list", f"HEAD..origin/{branch}", "--count"], ...)
commit_count = int(result.stdout.strip())
if commit_count == 0:
    # ... restore the stash we never should have created
# BEFORE — hermes_cli/main.py:9574
fetch_result = subprocess.run(
    git_cmd + ["fetch", "origin", branch],
    cwd=PROJECT_ROOT,
    capture_output=True,  # ← swallows git progress
    text=True,
)

Fix

1. Visible fetch progress

Switch the user-facing fetch to capture_output=False. On failure, re-run with capture_output=True so we can still pattern-match stderr for network/auth errors (the existing Could not resolve host, Authentication failed, etc. logic is preserved).

2. Deferred stash

Move _stash_local_changes_if_needed to AFTER the rev-list count check. If commit_count == 0, we exit cleanly without ever creating a stash. The race condition where someone else pulls between our check and our stash is still handled correctly by the post-stash commit_count == 0 block.

3. Removed redundant rev-list re-run

The original code ran rev-list twice (once before the stash, once after). Since git stash doesn't move HEAD, the second call was redundant. Removed.

Tests Added

Three new tests in tests/hermes_cli/test_cmd_update.py:

  • test_3523_no_stash_when_up_to_date_with_local_changes — asserts git stash push is NOT called when up to date, even with dirty tree
  • test_3523_fetch_stderr_visible_on_success — asserts user-facing fetch has capture_output=False
  • test_3523_stash_still_happens_when_update_available — asserts stash still happens when update IS available (regression guard for the deferred-stash fix)

All 30 tests pass (27 existing + 3 new).

Checklist

  • Reproduces on current main
  • Points to exact line where bug manifests
  • Fixes the whole bug class (sibling call paths included) — both fetch output suppression AND autostash are fixed
  • E2E validation — all 30 tests in test_cmd_update.py pass
  • Behavior contracts (invariant: no stash when count==0) over snapshots
  • Cache-, alternation-, and invariant-safe — no changes to the conversation loop or system prompt
  • Prompt caching preserved — capture_output doesn't affect what gets cached
  • No new HERMES_* env vars
  • No outbound telemetry added

…l update confirmed (NousResearch#3523)

Regression from NousResearch#3492 introduced two user-visible issues in `hermes update`:

1. `git fetch` was run with `capture_output=True`, swallowing git's
   progress output (object enumeration, remote branch listing).
2. `git stash push` ran BEFORE we knew whether there was an update to
   apply. On an up-to-date tree with local changes, every `hermes update`
   call would autostash-and-pop for nothing — cluttering the stash list
   and triggering an interactive restore prompt.

Fix:
- Switch the user-facing fetch to `capture_output=False` so progress
  reaches the terminal. Re-run with `capture_output=True` only on
  failure so we can still pattern-match stderr for network/auth errors.
- Move the `git status` + `git stash push` block to AFTER the
  `git rev-list HEAD..origin/main --count` check. If the count is 0,
  no stash is created and we exit cleanly.
- The race where someone else pulls between our check and our stash
  is still handled by the post-stash `commit_count == 0` block.

Tests:
- `test_3523_no_stash_when_up_to_date_with_local_changes`: verifies no
  `git stash push` is called when up to date.
- `test_3523_fetch_stderr_visible_on_success`: verifies the user-facing
  fetch uses `capture_output=False`.
- `test_3523_stash_still_happens_when_update_available`: verifies the
  fix didn't break the normal update-with-local-changes path.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels Jul 12, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating two real hermes update regressions. The current checkout still captures fetch output at hermes_cli/main.py:9573 and stashes before the count check at hermes_cli/main.py:9650.

Problems

  • The deferred-stash change removes the protection that main intentionally applies before a cross-branch checkout (hermes_cli/main.py:9616). If HEAD differs from the target and a local edit would be overwritten, git checkout <branch> fails before the new post-rev-list stash path can run. The fallback checkout then reports the branch missing even though the local edit is the blocker.

Suggested changes

  • Preserve local changes before a required branch switch, or compute whether the target branch is ahead without checking it out and only then stash before checkout.
  • Add a regression test for dirty, conflicting edits while switching branches; the added tests cover only the same-branch path.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
@teknium1 teknium1 added the area/install-update Installer, updater, packaging, wheels, doctor label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes update regressions after #3492 — silent git output and unnecessary stashes

3 participants