{"repo":"NousResearch/hermes-agent","public_id":"38122","display_label":"Desktop auto-update reliability and dirty-tree failures","summary":"A cluster of bugs and fixes around the desktop/Hermes update pipeline leaving users in unrecoverable states: bundling artifacts dirtying the git working tree, stash/restore logic clobbering or failing to restore files, broken binaries with no repair path, and an infinite update loop on Windows. The fixes address git index conflicts, stash safety, binary preservation on build failure, and venv validation.","subsystem":"apps/bootstrap-installer","counts":{"members":13,"open_issues":2,"open_issues_distinct":2,"open_prs":0,"closed_issues":7,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54135},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-28T09:40:38+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.1112820512820513},"fix_edges":[{"issue_number":38122,"pr_number":54135,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":45542,"pr_number":54135,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":38122,"title":"Windows Desktop update can loop after dirty bundled repo checkout and broken venv","url":null,"state":"closed"},{"kind":"issue","number":46763,"title":"casual users enter unrecoverable state: desktop update fails on dirty tree → built exe no longer launches → redownload/reinstall also fails on the same dirty git state","url":null,"state":"closed"},{"kind":"issue","number":46782,"title":"No automatic recovery or repair path after desktop update handoff fails, leaving users unable to launch or reinstall","url":null,"state":"closed"},{"kind":"issue","number":46791,"title":"Repository stage stash/local-changes handling is fragile and can leave the tree in unrecoverable dirty state after interrupted updates","url":null,"state":"closed"},{"kind":"issue","number":46792,"title":"Rebuild step after successful repo update can leave desktop binary broken with no recovery path","url":null,"state":"open"},{"kind":"issue","number":53081,"title":"PRD: robust autostash backup, restore, and failure notification for Hermes update","url":null,"state":"open"},{"kind":"issue","number":53224,"title":"Hermes Desktop updater dirties source checkout by bundling main.cjs in place","url":null,"state":"closed"},{"kind":"issue","number":53949,"title":"[Bug]: apps/desktop/scripts/bundle-electron-main.mjs overwrites source main.cjs in working tree","url":null,"state":"closed"},{"kind":"issue","number":55417,"title":"desktop: bundle-electron-main.mjs overwrites tracked electron/main.cjs in place, dirtying the tree and blocking git auto-update","url":null,"state":"closed"},{"kind":"pr","number":38179,"title":"fix(desktop): recover dirty Windows install checkout","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54135","number":54135},{"kind":"pr","value":"#52378","number":52378},{"kind":"pr","value":"#38542","number":38542}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":51390,"title":"fix(install): reset tree on conflicting stash apply so Hermes stays runnable","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":53241,"title":"fix(desktop): stage bundled electron main","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2d206a3a4","number":0},{"kind":"pr","value":"#52735","number":52735}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":54135,"title":"fix(windows): break the desktop auto-update infinite loop (#52378)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/backend-probes.cjs","apps/desktop/electron/backend-probes.test.cjs","apps/desktop/electron/main.cjs","apps/desktop/electron/windows-hermes-resolution.test.cjs","apps/desktop/package.json","hermes_cli/main.py","infographic/windows-update-loop-52378/infographic.png","scripts/release.py","tests/hermes_cli/test_update_interrupted_recovery.py"],"fix_hunks":[{"file":"apps/desktop/electron/backend-probes.cjs","hunks":["@@ -37,7 +37,18 @@ const { execFileSync } = require('node:child_process')\n const PROBE_TIMEOUT_MS = 5000\n \n /**\n- * Return true iff `python -c \"import hermes_cli\"` exits 0.\n+ * Return the Python snippet used to verify Hermes can import far enough to\n+ * launch the CLI. Kept exported for tests so dependency regressions are\n+ * caught without needing a real broken venv fixture.\n+ *\n+ * @returns {string}\n+ */\n+function hermesRuntimeImportProbe() {\n+  return 'import yaml; import hermes_cli.config'\n+}\n+\n+/**\n+ * Return true iff the Hermes runtime import probe exits 0.\n  *\n  * Used to gate the \"fallback to system Python with hermes_cli installed\"\n  * rung of resolveHermesBackend. Without this, a system Python 3.11-3.13","@@ -46,13 +57,20 @@ const PROBE_TIMEOUT_MS = 5000\n  * site-packages -- and the resolver returns a backend that immediately\n  * dies on spawn.\n  *\n+ * The probe intentionally imports hermes_cli.config, not just the top-level\n+ * package: a broken/empty Windows launcher venv can still see the source tree\n+ * through PYTHONPATH but lack PyYAML, then die on the first real CLI import.\n+ *\n  * @param {string} pythonPath - Absolute path to a python.exe / python.\n+ * @param {object} [opts]\n+ * @param {object} [opts.env] - Additional environment for the probe.\n  * @returns {boolean}\n  */\n-function canImportHermesCli(pythonPath) {\n+function canImportHermesCli(pythonPath, opts = {}) {\n   if (!pythonPath) return false\n   try {\n-    execFileSync(pythonPath, ['-c', 'import hermes_cli'], {\n+    execFileSync(pythonPath, ['-c', hermesRuntimeImportProbe()], {\n+      env: { ...process.env, ...(opts.env || {}) },\n       stdio: 'ignore',\n       timeout: PROBE_TIMEOUT_MS,\n       windowsHide: true","@@ -101,6 +119,7 @@ function verifyHermesCli(hermesCommand, opts = {}) {\n \n module.exports = {\n   canImportHermesCli,\n+  hermesRuntimeImportProbe,\n   verifyHermesCli,\n   PROBE_TIMEOUT_MS\n }"]},{"file":"apps/desktop/electron/backend-probes.test.cjs","hunks":["@@ -11,7 +11,7 @@ const fs = require('node:fs')\n const os = require('node:os')\n const path = require('node:path')\n \n-const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')\n+const { canImportHermesCli, hermesRuntimeImportProbe, verifyHermesCli } = require('./backend-probes.cjs')\n \n // Resolve the host's own Node binary -- guaranteed to be on disk and\n // runnable. We use it as both a stand-in for \"a python that doesn't","@@ -40,6 +40,12 @@ test('canImportHermesCli returns false when binary does not exist', () => {\n   assert.equal(canImportHermesCli(ghost), false)\n })\n \n+test('hermes runtime import probe checks config dependencies', () => {\n+  const probe = hermesRuntimeImportProbe()\n+  assert.match(probe, /\\bimport yaml\\b/)\n+  assert.match(probe, /\\bimport hermes_cli\\.config\\b/)\n+})\n+\n test('verifyHermesCli returns false when command is falsy', () => {\n   assert.equal(verifyHermesCli(''), false)\n   assert.equal(verifyHermesCli(null), false)"]},{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -1285,8 +1285,14 @@ function findOnPath(command) {\n   const pathEntries = String(process.env.PATH || '')\n     .split(path.delimiter)\n     .filter(Boolean)\n+  // On Windows, try PATHEXT extensions BEFORE the bare (empty-extension) name.\n+  // A real command must resolve via its .exe/.cmd (Windows command-resolution\n+  // semantics consult PATHEXT); an extensionless file — e.g. a Git-Bash\n+  // shell-script shim named `hermes` — must not shadow `hermes.cmd`/`hermes.exe`.\n+  // The empty entry is kept LAST so callers that already include the extension\n+  // (py.exe, pwsh.exe, powershell.exe) still resolve.\n   const extensions = IS_WINDOWS\n-    ? ['', ...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]\n+    ? [...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean), '']\n     : ['']\n \n   for (const entry of pathEntries) {","@@ -2243,7 +2249,18 @@ async function handOffWindowsBootstrapRecovery(reason) {\n     : configuredBranch || DEFAULT_UPDATE_BRANCH\n   const venvBin = path.join(updateRoot, 'venv', IS_WINDOWS ? 'Scripts' : 'bin')\n   const venvHermes = path.join(venvBin, IS_WINDOWS ? 'hermes.exe' : 'hermes')\n-  const updaterArgs = fileExists(venvHermes) ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]\n+  const venvPython = path.join(venvBin, IS_WINDOWS ? 'python.exe' : 'python')\n+  // Choose the gentle in-place --update when ANY real-install signal is present,\n+  // not just the `hermes.exe` console-script shim. That shim is generated at the\n+  // END of venv setup and is absent in exactly the interrupted/quarantined states\n+  // this recovery exists to heal — gating on it alone forced the destructive\n+  // --repair (full venv recreate) and drove reinstall loops. The venv interpreter\n+  // and the bootstrap-complete marker are present earlier and are better signals.\n+  const haveRealInstall =\n+    fileExists(venvPython) ||\n+    fileExists(venvHermes) ||\n+    fileExists(path.join(updateRoot, '.hermes-bootstrap-complete'))\n+  const updaterArgs = haveRealInstall ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]\n \n   await releaseBackendLockForUpdate(updateRoot)\n ","@@ -2608,6 +2625,24 @@ function readBootstrapMarker() {\n   return readJson(BOOTSTRAP_COMPLETE_MARKER)\n }\n \n+// Marker-independent: is the canonical install at ACTIVE_HERMES_ROOT actually\n+// runnable right now? A complete CLI install (`install.sh --include-desktop`)\n+// or a DMG launch over a prior CLI install satisfies this WITHOUT the desktop\n+// ever having written the bootstrap marker -- so we must be able to recognise\n+// \"already installed\" off the filesystem alone, not just the marker.\n+function isActiveRuntimeUsable() {\n+  const venvPython = getVenvPython(VENV_ROOT)\n+  return (\n+    isHermesSourceRoot(ACTIVE_HERMES_ROOT) &&\n+    fileExists(venvPython) &&\n+    canImportHermesCli(venvPython, {\n+      env: {\n+        PYTHONPATH: [ACTIVE_HERMES_ROOT, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter)\n+      }\n+    })\n+  )\n+}\n+\n function isBootstrapComplete() {\n   const marker = readBootstrapMarker()\n   if (!marker || typeof marker !== 'object') return false","@@ -2620,7 +2655,7 @@ function isBootstrapComplete() {\n   // a runnable venv: an interrupted or split-home install can leave the marker\n   // + checkout without a venv, and trusting that spawns a dead backend\n   // (\"gateway offline\") instead of re-running bootstrap to repair it.\n-  return isHermesSourceRoot(ACTIVE_HERMES_ROOT) && fileExists(getVenvPython(VENV_ROOT))\n+  return isActiveRuntimeUsable()\n }\n \n function writeBootstrapMarker(payload) {"]},{"file":"apps/desktop/electron/windows-hermes-resolution.test.cjs","hunks":["@@ -0,0 +1,67 @@\n+'use strict'\n+\n+// Regression guards for Windows `hermes` resolution in main.cjs.\n+//\n+// main.cjs has no module.exports, so these follow the repo's source-assertion\n+// test pattern (see windows-child-process.test.cjs). They pin the two Windows\n+// resolution bugs that caused desktop reinstall loops:\n+//   1. findOnPath() tried the empty extension FIRST, so an extensionless\n+//      Git-Bash `hermes` shim shadowed the real hermes.cmd/hermes.exe; the\n+//      shim then failed the --version probe and the desktop fell through to a\n+//      spurious bootstrap/repair.\n+//   2. handOffWindowsBootstrapRecovery() chose --update vs the destructive\n+//      --repair by checking ONLY venv\\Scripts\\hermes.exe (the console-script\n+//      shim, written at the END of venv setup and absent in interrupted\n+//      states), so it escalated to a full venv recreate even on healthy\n+//      installs.\n+\n+const test = require('node:test')\n+const assert = require('node:assert/strict')\n+const fs = require('node:fs')\n+const path = require('node:path')\n+\n+function readMain() {\n+  return fs.readFileSync(path.join(__dirname, 'main.cjs'), 'utf8').replace(/\\r\\n/g, '\\n')\n+}\n+\n+test('findOnPath tries PATHEXT extensions before the bare (empty) name on Windows', () => {\n+  const source = readMain()\n+  // Fixed order: PATHEXT first, empty string LAST.\n+  assert.match(\n+    source,\n+    /\\(process\\.env\\.PATHEXT \\|\\| '\\.COM;\\.EXE;\\.BAT;\\.CMD'\\)\\.split\\(';'\\)\\.filter\\(Boolean\\), ''\\]/,\n+    'extensions array must end with the empty string, not start with it'\n+  )\n+  // The buggy empty-first order must not return.\n+  assert.doesNotMatch(\n+    source,\n+    /\\['', \\.\\.\\.\\(process\\.env\\.PATHEXT/,\n+    'empty-extension-first order regressed: an extensionless shim can shadow hermes.cmd/.exe'\n+  )\n+})\n+\n+test('Windows bootstrap recovery chooses --update when any real-install signal is present', () => {\n+  const source = readMain()\n+  assert.match(source, /const haveRealInstall =/, 'recovery must compute haveRealInstall')\n+  assert.match(\n+    source,\n+    /fileExists\\(venvPython\\)/,\n+    'recovery must accept the venv interpreter as a real-install signal'\n+  )\n+  assert.match(\n+    source,\n+    /\\.hermes-bootstrap-complete/,\n+    'recovery must accept the bootstrap-complete marker as a real-install signal'\n+  )\n+  assert.match(\n+    source,\n+    /updaterArgs = haveRealInstall \\? \\['--update'/,\n+    'updaterArgs must gate on haveRealInstall'\n+  )\n+  // The old too-narrow check (only venv\\Scripts\\hermes.exe) must not return.\n+  assert.doesNotMatch(\n+    source,\n+    /updaterArgs = fileExists\\(venvHermes\\) \\?/,\n+    'recovery regressed to gating only on the hermes.exe shim, which forces destructive --repair'\n+  )\n+})"]},{"file":"apps/desktop/package.json","hunks":["@@ -37,7 +37,7 @@\n     \"test:desktop:nsis\": \"node scripts/test-desktop.mjs nsis\",\n     \"test:desktop:existing\": \"node scripts/test-desktop.mjs existing\",\n     \"test:desktop:fresh\": \"node scripts/test-desktop.mjs fresh\",\n-    \"test:desktop:platforms\": \"node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs\",\n+    \"test:desktop:platforms\": \"node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs electron/windows-hermes-resolution.test.cjs\",\n     \"typecheck\": \"tsc -p . --noEmit\",\n     \"lint\": \"eslint src/ electron/\",\n     \"lint:fix\": \"eslint src/ electron/ --fix\","]},{"file":"hermes_cli/main.py","hunks":["@@ -6992,6 +6992,55 @@ def _recover_from_interrupted_install() -> None:\n         # the install itself will surface the real problem.\n         logger.debug(\"Could not create install-recovery lock: %s\", exc)\n \n+    # Windows self-lock guard: if hermes.exe is the launcher that spawned\n+    # this Python process, any attempt to pip-install will fail with\n+    # \"拒绝访问 / WinError 32\" because the running .exe cannot be replaced.\n+    # Rather than entering the permanent retry loop described in issue\n+    # #45542, clear the marker and give the user an offline recovery command.\n+    if _is_windows():\n+        scripts_dir = _venv_scripts_dir()\n+        if scripts_dir is not None:\n+            shims = _hermes_exe_shims(scripts_dir)\n+            if shims:\n+                _shim_set: set[str] = set()\n+                for _s in shims:\n+                    try:\n+                        _shim_set.add(str(_s.resolve()).lower())\n+                    except OSError:\n+                        _shim_set.add(str(_s).lower())\n+                try:\n+                    import psutil\n+                    _me = psutil.Process()\n+                    for _anc in [_me] + list(_me.parents()):\n+                        try:\n+                            _anc_exe = _anc.exe()\n+                            _anc_norm = str(Path(_anc_exe).resolve()).lower()\n+                        except Exception:\n+                            continue\n+                        if _anc_norm in _shim_set:\n+                            print(\n+                                \"✗ Hermes is running from the binary that \"\n+                                \"needs to be replaced — the auto-recovery \"\n+                                \"cannot overwrite a running executable.\"\n+                            )\n+                            print(\n+                                \"  Restart Hermes from a different terminal, \"\n+                                \"then run the manual recovery command below:\"\n+                            )\n+                            print(f'    cd /d \"{PROJECT_ROOT}\"')\n+                            print(\n+                                f'    \"{sys.executable}\" -m pip install '\n+                                '-e \".[all]\"'\n+                            )\n+                            _clear_update_incomplete_marker()\n+                            try:\n+                                lock_path.unlink()\n+                            except OSError:\n+                                pass\n+                            return\n+                except Exception:\n+                    pass  # psutil is best-effort; fall through to install\n+\n     saved_stdout_fd = None\n     saved_sys_stdout = sys.stdout\n     try:"]},{"file":"scripts/release.py","hunks":["@@ -167,6 +167,8 @@\n     \"[redacted:email]\": \"synapsesx\",\n     \"[redacted:email]\": \"itsflownium\",\n     \"[redacted:email]\": \"dirtyren\",\n+    \"[redacted:email]\": \"Cossackx\",  # PR #52528 salvage (Windows hermes-shim resolution + prefer --update on recovery; #52378)\n+    \"[redacted:email]\": \"Icather\",  # PR #45554 salvage (self-lock guard breaks Windows update-recovery infinite loop; #52378 / #45542)\n     \"--email\": \"[redacted:email]\",\n     \"[redacted:email]\": \"francescomucio\",\n     \"[redacted:email]\": \"thestral123\","]},{"file":"tests/hermes_cli/test_update_interrupted_recovery.py","hunks":["@@ -139,6 +139,90 @@ class R:\n     )\n \n \n+def test_recovery_self_lock_guard_clears_marker_without_install(tmp_path, monkeypatch):\n+    # Windows self-lock: hermes.exe is an ancestor of this Python process, so a\n+    # pip-install would fail trying to replace the running launcher (WinError 32\n+    # / 拒绝访问). Recovery must short-circuit — clear the marker, skip install,\n+    # break the loop (#45542 / #52378) — instead of retrying forever.\n+    monkeypatch.setattr(m, \"PROJECT_ROOT\", tmp_path)\n+    (tmp_path / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n+    m._write_update_incomplete_marker()\n+\n+    scripts_dir = tmp_path / \"venv\" / \"Scripts\"\n+    scripts_dir.mkdir(parents=True)\n+    shim = scripts_dir / \"hermes.exe\"\n+    shim.write_text(\"\")\n+\n+    monkeypatch.setattr(m, \"_is_windows\", lambda: True)\n+    monkeypatch.setattr(m, \"_venv_scripts_dir\", lambda: scripts_dir)\n+    monkeypatch.setattr(m, \"_hermes_exe_shims\", lambda d: [shim])\n+\n+    class FakeProc:\n+        def __init__(self, exe_path):\n+            self._exe = exe_path\n+\n+        def exe(self):\n+            return self._exe\n+\n+        def parents(self):\n+            return [FakeProc(str(shim))]\n+\n+    monkeypatch.setattr(\"psutil.Process\", lambda: FakeProc(sys_executable_path()))\n+\n+    seen = {\"install\": False}\n+    _stub_install_env(monkeypatch, m, seen)\n+\n+    m._recover_from_interrupted_install()\n+\n+    assert seen[\"install\"] is False, \"self-lock must skip the install\"\n+    assert not m._update_marker_path().exists(), \"marker cleared to break the loop\"\n+\n+\n+def sys_executable_path():\n+    import sys\n+\n+    return sys.executable\n+\n+\n+def test_recovery_self_lock_guard_inactive_when_not_ancestor(tmp_path, monkeypatch):\n+    # Windows, but hermes.exe is NOT in the ancestry (launched via `hermes\n+    # dashboard` from a separate cmd, say). The guard must fall through to the\n+    # normal install so a genuinely interrupted install still gets healed.\n+    monkeypatch.setattr(m, \"PROJECT_ROOT\", tmp_path)\n+    (tmp_path / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n+    m._write_update_incomplete_marker()\n+\n+    scripts_dir = tmp_path / \"venv\" / \"Scripts\"\n+    scripts_dir.mkdir(parents=True)\n+    shim = scripts_dir / \"hermes.exe\"\n+    shim.write_text(\"\")\n+\n+    monkeypatch.setattr(m, \"_is_windows\", lambda: True)\n+    monkeypatch.setattr(m, \"_venv_scripts_dir\", lambda: scripts_dir)\n+    monkeypatch.setattr(m, \"_hermes_exe_shims\", lambda d: [shim])\n+\n+    class FakeProc:\n+        def __init__(self, exe_path):\n+            self._exe = exe_path\n+\n+        def exe(self):\n+            return self._exe\n+\n+        def parents(self):\n+            # Ancestry is plain pythons / cmd — no hermes.exe shim.\n+            return [FakeProc(str(tmp_path / \"cmd.exe\"))]\n+\n+    monkeypatch.setattr(\"psutil.Process\", lambda: FakeProc(sys_executable_path()))\n+\n+    seen = {\"install\": False}\n+    _stub_install_env(monkeypatch, m, seen)\n+\n+    m._recover_from_interrupted_install()\n+\n+    assert seen[\"install\"] is True, \"without self-lock, normal recovery runs\"\n+    assert not m._update_marker_path().exists(), \"marker cleared on successful install\"\n+\n+\n def test_recovery_skips_when_lock_held(tmp_path, monkeypatch):\n     # Another process is mid-recovery (fresh lockfile) — this launch must skip\n     # the install entirely and leave both marker and lock untouched.\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}