{"repo":"NousResearch/hermes-agent","public_id":"48352","display_label":"PowerShell EAP=Stop causing spurious install failures","summary":"These issues and PRs address a Windows install.ps1 problem where PowerShell 5.1's ErrorActionPreference=Stop incorrectly treats stderr output from native git/uv calls as fatal errors. The proposed fixes relax error handling around those native calls to prevent spurious installation failures.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48529,"merged_at":"2026-06-18T16:47:54+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-18T16:47:54+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":48352,"pr_number":48529,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T16:47:54+00:00"}],"members":[{"kind":"issue","number":48352,"title":"install.ps1 (Windows): PowerShell 5.1 NativeCommandError wrapping of git/uv stderr causes spurious install failures","url":null,"state":"closed"},{"kind":"pr","number":48372,"title":"fix(install): relax native stderr handling in install.ps1 (#48352)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48529","number":48529},{"kind":"pr","value":"#48352","number":48352}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":48463,"title":"fix(install): relax EAP=Stop around git/uv native calls in install.ps1 (#48352)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48529","number":48529},{"kind":"pr","value":"#48372","number":48372}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":48529,"title":"fix(install): relax EAP=Stop around native git/uv calls + fail-fast on uv venv failure (#48352, salvage of #48372)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/install.ps1","tests/test_install_ps1_native_stderr_eap.py"],"fix_hunks":[{"file":"scripts/install.ps1","hunks":["@@ -185,6 +185,18 @@ function Write-Err {\n     Write-Host \"[X] $Message\" -ForegroundColor Red\n }\n \n+function Invoke-NativeWithRelaxedErrorAction {\n+    param([scriptblock]$Script)\n+\n+    $prevEAP = $ErrorActionPreference\n+    $ErrorActionPreference = \"Continue\"\n+    try {\n+        & $Script\n+    } finally {\n+        $ErrorActionPreference = $prevEAP\n+    }\n+}\n+\n # Inspect npm output for a TLS-trust failure and, if found, print actionable\n # remediation. npm/Node surface corporate MITM proxies and missing root CAs as\n # \"unable to get local issuer certificate\" / \"self-signed certificate in","@@ -1306,7 +1318,7 @@ function Install-Repository {\n         Write-Info \"Trying SSH clone...\"\n         $env:GIT_SSH_COMMAND = \"ssh -o BatchMode=yes -o ConnectTimeout=5\"\n         try {\n-            git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlSsh $InstallDir\n+            Invoke-NativeWithRelaxedErrorAction { git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlSsh $InstallDir }\n             if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }\n         } catch { }\n         $env:GIT_SSH_COMMAND = $null","@@ -1315,7 +1327,7 @@ function Install-Repository {\n             if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }\n             Write-Info \"SSH failed, trying HTTPS...\"\n             try {\n-                git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlHttps $InstallDir\n+                Invoke-NativeWithRelaxedErrorAction { git -c windows.appendAtomically=false clone --depth 1 --branch $Branch $RepoUrlHttps $InstallDir }\n                 if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }\n             } catch { }\n         }","@@ -1443,8 +1455,20 @@ function Install-Venv {\n         Remove-Item -Recurse -Force \"venv\"\n     }\n     \n-    # uv creates the venv and pins the Python version in one step\n-    & $UvCmd venv venv --python $PythonVersion\n+    # uv creates the venv and pins the Python version in one step.  uv emits\n+    # normal progress such as \"Using CPython ...\" on stderr; under Windows\n+    # PowerShell 5.1 with EAP=Stop that stderr is a NativeCommandError unless\n+    # we temporarily relax EAP and trust $LASTEXITCODE for real failures.\n+    Invoke-NativeWithRelaxedErrorAction { & $UvCmd venv venv --python $PythonVersion }\n+    # Relaxing EAP above means a *genuine* uv-venv failure (exit != 0) no longer\n+    # aborts on its own. Capture $LASTEXITCODE immediately and fail fast, so the\n+    # `venv` stage can't falsely report success (and Invoke-Stage can't emit\n+    # ok=true) when the venv was never created.\n+    $venvExitCode = $LASTEXITCODE\n+    if ($venvExitCode -ne 0) {\n+        Pop-Location\n+        throw \"Failed to create virtual environment (uv venv exited with $venvExitCode)\"\n+    }\n \n     # Neutralize any inherited UV_PYTHON (e.g. $env:UV_PYTHON = \"3.14\" left in\n     # the user's shell). uv honours UV_PYTHON over an existing venv for the","@@ -1514,7 +1538,7 @@ function Install-Dependencies {\n         # in the wrong directory and imports fail with ModuleNotFoundError.\n         # (Mirrors the same flag in scripts/install.sh::install_deps.)\n         $env:UV_PROJECT_ENVIRONMENT = \"$InstallDir\\venv\"\n-        & $UvCmd sync --extra all --locked\n+        Invoke-NativeWithRelaxedErrorAction { & $UvCmd sync --extra all --locked }\n         if ($LASTEXITCODE -eq 0) {\n             Write-Success \"Main package installed (hash-verified via uv.lock)\"\n             $script:InstalledTier = \"hash-verified (uv.lock)\"","@@ -1589,7 +1613,7 @@ except Exception:\n     if (-not $skipPipFallback) {\n         foreach ($tier in $installTiers) {\n         Write-Info \"Trying tier: $($tier.Name) ...\"\n-        & $UvCmd pip install -e $tier.Spec\n+        Invoke-NativeWithRelaxedErrorAction { & $UvCmd pip install -e $tier.Spec }\n         if ($LASTEXITCODE -eq 0) {\n             Write-Success \"Main package installed ($($tier.Name))\"\n             $script:InstalledTier = $tier.Name"]},{"file":"tests/test_install_ps1_native_stderr_eap.py","hunks":["@@ -0,0 +1,94 @@\n+\"\"\"Regression tests for #48352: Windows PowerShell 5.1 native stderr.\n+\n+PowerShell 5.1 turns stderr from native commands into ``NativeCommandError``\n+records when ``$ErrorActionPreference = \"Stop\"``.  ``scripts/install.ps1`` has a\n+few git/uv calls where stderr can be normal progress output, so those calls must\n+run with EAP temporarily relaxed and then inspect ``$LASTEXITCODE``.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import re\n+from pathlib import Path\n+\n+REPO_ROOT = Path(__file__).resolve().parent.parent\n+INSTALL_PS1 = REPO_ROOT / \"scripts\" / \"install.ps1\"\n+\n+\n+def _install_ps1() -> str:\n+    return INSTALL_PS1.read_text(encoding=\"utf-8\")\n+\n+\n+def _assert_relaxed_call(text: str, command_pattern: str) -> None:\n+    helper_block_pattern = (\n+        r\"Invoke-NativeWithRelaxedErrorAction\\s*\\{[^}]*\"\n+        + command_pattern\n+        + r\"[^}]*\\}\"\n+    )\n+    inline_pattern = (\n+        r\"\\$ErrorActionPreference\\s*=\\s*\\\"Continue\\\"[\\s\\S]{0,900}?\"\n+        + command_pattern\n+    )\n+    assert re.search(helper_block_pattern, text) or re.search(inline_pattern, text), (\n+        f\"install.ps1 must relax ErrorActionPreference around {command_pattern}\"\n+    )\n+\n+\n+def test_repository_stage_relieves_eap_for_ssh_and_https_git_clone() -> None:\n+    text = _install_ps1()\n+    assert \"function Invoke-NativeWithRelaxedErrorAction\" in text\n+    _assert_relaxed_call(\n+        text,\n+        r\"git -c windows\\.appendAtomically=false clone --depth 1 --branch \\$Branch \\$RepoUrlSsh \\$InstallDir\",\n+    )\n+    _assert_relaxed_call(\n+        text,\n+        r\"git -c windows\\.appendAtomically=false clone --depth 1 --branch \\$Branch \\$RepoUrlHttps \\$InstallDir\",\n+    )\n+\n+\n+def test_uv_venv_and_dependency_installs_relax_eap() -> None:\n+    text = _install_ps1()\n+    _assert_relaxed_call(text, r\"& \\$UvCmd venv venv --python \\$PythonVersion\")\n+    _assert_relaxed_call(text, r\"& \\$UvCmd sync --extra all --locked\")\n+    _assert_relaxed_call(text, r\"& \\$UvCmd pip install -e \\$tier\\.Spec\")\n+\n+\n+def test_uv_venv_failure_is_not_swallowed_after_eap_relax() -> None:\n+    \"\"\"Relaxing EAP must not let a genuine `uv venv` failure pass as success.\n+\n+    Once EAP is relaxed, a real non-zero `uv venv` exit no longer aborts on its\n+    own, so install.ps1 must capture $LASTEXITCODE right after the call and fail\n+    fast — otherwise the `venv` stage falsely reports success (Invoke-Stage emits\n+    ok=true) when no venv was created. Regression guard for the gap caught while\n+    reviewing #48372 (the explicit check originally proposed in #48463).\n+    \"\"\"\n+    text = _install_ps1()\n+    # The uv-venv invocation, then an exit-code capture, then a throw — all\n+    # within a small window after the relaxed call.\n+    guard = re.search(\n+        r\"& \\$UvCmd venv venv --python \\$PythonVersion[\\s\\S]{0,400}?\"\n+        r\"\\$LASTEXITCODE[\\s\\S]{0,200}?\"\n+        r\"-ne 0[\\s\\S]{0,200}?throw\",\n+        text,\n+    )\n+    assert guard is not None, (\n+        \"install.ps1 must capture uv venv's exit code and throw on failure after \"\n+        \"relaxing ErrorActionPreference, so a genuine venv-creation failure isn't \"\n+        \"reported as a successful stage\"\n+    )\n+\n+\n+def test_native_eap_helper_always_restores_previous_preference() -> None:\n+    text = _install_ps1()\n+    m = re.search(\n+        r\"function Invoke-NativeWithRelaxedErrorAction \\{(?P<body>[\\s\\S]*?)^\\}\",\n+        text,\n+        re.MULTILINE,\n+    )\n+    assert m is not None, \"expected a shared helper for NativeCommandError-safe calls\"\n+    body = m.group(\"body\")\n+    assert \"$prevEAP = $ErrorActionPreference\" in body\n+    assert '$ErrorActionPreference = \"Continue\"' in body\n+    assert \"finally\" in body\n+    assert \"$ErrorActionPreference = $prevEAP\" in body\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}